github.com/koko1123/flow-go-1@v0.29.6/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/v3" 10 "github.com/hashicorp/go-multierror" 11 "github.com/ipfs/go-cid" 12 "github.com/rs/zerolog" 13 14 "github.com/koko1123/flow-go-1/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 db, err := badger.Open(badger.LSMOnlyOptions(dbPath)) 193 if err != nil { 194 return nil, fmt.Errorf("could not open tracker db: %w", err) 195 } 196 197 storage := &storage{ 198 db: db, 199 pruneCallback: func(c cid.Cid) error { return nil }, 200 logger: logger.With().Str("module", "tracker_storage").Logger(), 201 } 202 203 for _, opt := range opts { 204 opt(storage) 205 } 206 207 if err := storage.init(startHeight); err != nil { 208 return nil, fmt.Errorf("failed to initialize storage: %w", err) 209 } 210 211 return storage, nil 212 } 213 214 func (s *storage) init(startHeight uint64) error { 215 fulfilledHeight, fulfilledHeightErr := s.GetFulfilledHeight() 216 prunedHeight, prunedHeightErr := s.GetPrunedHeight() 217 218 if fulfilledHeightErr == nil && prunedHeightErr == nil { 219 if prunedHeight > fulfilledHeight { 220 return fmt.Errorf( 221 "inconsistency detected: pruned height (%d) is greater than fulfilled height (%d)", 222 prunedHeight, 223 fulfilledHeight, 224 ) 225 } 226 227 // replay pruning in case it was interrupted during previous shutdown 228 if err := s.PruneUpToHeight(prunedHeight); err != nil { 229 return fmt.Errorf("failed to replay pruning: %w", err) 230 } 231 } else if errors.Is(fulfilledHeightErr, badger.ErrKeyNotFound) && errors.Is(prunedHeightErr, badger.ErrKeyNotFound) { 232 // db is empty, we need to bootstrap it 233 if err := s.bootstrap(startHeight); err != nil { 234 return fmt.Errorf("failed to bootstrap storage: %w", err) 235 } 236 } else { 237 return multierror.Append(fulfilledHeightErr, prunedHeightErr).ErrorOrNil() 238 } 239 240 return nil 241 } 242 243 func (s *storage) bootstrap(startHeight uint64) error { 244 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 245 fulfilledHeightValue := makeUint64Value(startHeight) 246 247 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 248 prunedHeightValue := makeUint64Value(startHeight) 249 250 return s.db.Update(func(txn *badger.Txn) error { 251 if err := txn.Set(fulfilledHeightKey, fulfilledHeightValue); err != nil { 252 return fmt.Errorf("failed to set fulfilled height value: %w", err) 253 } 254 255 if err := txn.Set(prunedHeightKey, prunedHeightValue); err != nil { 256 return fmt.Errorf("failed to set pruned height value: %w", err) 257 } 258 259 return nil 260 }) 261 } 262 263 func (s *storage) Update(f UpdateFn) error { 264 s.mu.RLock() 265 defer s.mu.RUnlock() 266 return f(s.trackBlobs) 267 } 268 269 func (s *storage) SetFulfilledHeight(height uint64) error { 270 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 271 fulfilledHeightValue := makeUint64Value(height) 272 273 return s.db.Update(func(txn *badger.Txn) error { 274 if err := txn.Set(fulfilledHeightKey, fulfilledHeightValue); err != nil { 275 return fmt.Errorf("failed to set fulfilled height value: %w", err) 276 } 277 278 return nil 279 }) 280 } 281 282 func (s *storage) GetFulfilledHeight() (uint64, error) { 283 fulfilledHeightKey := makeGlobalStateKey(globalStateFulfilledHeight) 284 var fulfilledHeight uint64 285 286 if err := s.db.View(func(txn *badger.Txn) error { 287 item, err := txn.Get(fulfilledHeightKey) 288 if err != nil { 289 return fmt.Errorf("failed to find fulfilled height entry: %w", err) 290 } 291 292 fulfilledHeight, err = getUint64Value(item) 293 if err != nil { 294 return fmt.Errorf("failed to retrieve fulfilled height value: %w", err) 295 } 296 297 return nil 298 }); err != nil { 299 return 0, err 300 } 301 302 return fulfilledHeight, nil 303 } 304 305 func (s *storage) trackBlob(txn *badger.Txn, blockHeight uint64, c cid.Cid) error { 306 if err := txn.Set(makeBlobRecordKey(blockHeight, c), nil); err != nil { 307 return fmt.Errorf("failed to add blob record: %w", err) 308 } 309 310 latestHeightKey := makeLatestHeightKey(c) 311 item, err := txn.Get(latestHeightKey) 312 if err != nil { 313 if !errors.Is(err, badger.ErrKeyNotFound) { 314 return fmt.Errorf("failed to get latest height: %w", err) 315 } 316 } else { 317 latestHeight, err := getUint64Value(item) 318 if err != nil { 319 return fmt.Errorf("failed to retrieve latest height value: %w", err) 320 } 321 322 // don't update the latest height if there is already a higher block height containing this blob 323 if latestHeight >= blockHeight { 324 return nil 325 } 326 } 327 328 latestHeightValue := makeUint64Value(blockHeight) 329 330 if err := txn.Set(latestHeightKey, latestHeightValue); err != nil { 331 return fmt.Errorf("failed to set latest height value: %w", err) 332 } 333 334 return nil 335 } 336 337 func (s *storage) trackBlobs(blockHeight uint64, cids ...cid.Cid) error { 338 cidsPerBatch := cidsPerBatch 339 maxCidsPerBatch := getBatchItemCountLimit(s.db, 2, blobRecordKeyLength+latestHeightKeyLength+8) 340 if maxCidsPerBatch < cidsPerBatch { 341 cidsPerBatch = maxCidsPerBatch 342 } 343 344 for len(cids) > 0 { 345 batchSize := cidsPerBatch 346 if len(cids) < batchSize { 347 batchSize = len(cids) 348 } 349 batch := cids[:batchSize] 350 351 if err := retryOnConflict(s.db, func(txn *badger.Txn) error { 352 for _, c := range batch { 353 if err := s.trackBlob(txn, blockHeight, c); err != nil { 354 return fmt.Errorf("failed to track blob %s: %w", c.String(), err) 355 } 356 } 357 358 return nil 359 }); err != nil { 360 return err 361 } 362 363 cids = cids[batchSize:] 364 } 365 366 return nil 367 } 368 369 func (s *storage) batchDelete(deleteInfos []*deleteInfo) error { 370 return s.db.Update(func(txn *badger.Txn) error { 371 for _, dInfo := range deleteInfos { 372 if err := txn.Delete(makeBlobRecordKey(dInfo.height, dInfo.cid)); err != nil { 373 return fmt.Errorf("failed to delete blob record for Cid %s: %w", dInfo.cid.String(), err) 374 } 375 376 if dInfo.deleteLatestHeightRecord { 377 if err := txn.Delete(makeLatestHeightKey(dInfo.cid)); err != nil { 378 return fmt.Errorf("failed to delete latest height record for Cid %s: %w", dInfo.cid.String(), err) 379 } 380 } 381 } 382 383 return nil 384 }) 385 } 386 387 func (s *storage) batchDeleteItemLimit() int { 388 itemsPerBatch := 256 389 maxItemsPerBatch := getBatchItemCountLimit(s.db, 2, blobRecordKeyLength+latestHeightKeyLength) 390 if maxItemsPerBatch < itemsPerBatch { 391 itemsPerBatch = maxItemsPerBatch 392 } 393 return itemsPerBatch 394 } 395 396 func (s *storage) PruneUpToHeight(height uint64) error { 397 blobRecordPrefix := []byte{prefixBlobRecord} 398 itemsPerBatch := s.batchDeleteItemLimit() 399 var batch []*deleteInfo 400 401 s.mu.Lock() 402 defer s.mu.Unlock() 403 404 if err := s.setPrunedHeight(height); err != nil { 405 return err 406 } 407 408 if err := s.db.View(func(txn *badger.Txn) error { 409 it := txn.NewIterator(badger.IteratorOptions{ 410 PrefetchValues: false, 411 Prefix: blobRecordPrefix, 412 }) 413 defer it.Close() 414 415 // iterate over blob records, calling pruneCallback for any CIDs that should be pruned 416 // and cleaning up the corresponding tracker records 417 for it.Seek(blobRecordPrefix); it.ValidForPrefix(blobRecordPrefix); it.Next() { 418 blobRecordItem := it.Item() 419 blobRecordKey := blobRecordItem.Key() 420 421 blockHeight, blobCid, err := parseBlobRecordKey(blobRecordKey) 422 if err != nil { 423 return fmt.Errorf("malformed blob record key %v: %w", blobRecordKey, err) 424 } 425 426 // iteration occurs in key order, so block heights are guaranteed to be ascending 427 if blockHeight > height { 428 break 429 } 430 431 dInfo := &deleteInfo{ 432 cid: blobCid, 433 height: blockHeight, 434 } 435 436 latestHeightKey := makeLatestHeightKey(blobCid) 437 latestHeightItem, err := txn.Get(latestHeightKey) 438 if err != nil { 439 return fmt.Errorf("failed to get latest height entry for Cid %s: %w", blobCid.String(), err) 440 } 441 442 latestHeight, err := getUint64Value(latestHeightItem) 443 if err != nil { 444 return fmt.Errorf("failed to retrieve latest height value for Cid %s: %w", blobCid.String(), err) 445 } 446 447 // a blob is only removable if it is not referenced by any blob tree at a higher height 448 if latestHeight < blockHeight { 449 // this should never happen 450 return fmt.Errorf( 451 "inconsistency detected: latest height recorded for Cid %s is %d, but blob record exists at height %d", 452 blobCid.String(), latestHeight, blockHeight, 453 ) 454 } 455 456 // the current block height is the last to reference this CID, prune the CID and remove 457 // all tracker records 458 if latestHeight == blockHeight { 459 if err := s.pruneCallback(blobCid); err != nil { 460 return err 461 } 462 dInfo.deleteLatestHeightRecord = true 463 } 464 465 // remove tracker records for pruned heights 466 batch = append(batch, dInfo) 467 if len(batch) == itemsPerBatch { 468 if err := s.batchDelete(batch); err != nil { 469 return err 470 } 471 batch = nil 472 } 473 } 474 475 if len(batch) > 0 { 476 if err := s.batchDelete(batch); err != nil { 477 return err 478 } 479 } 480 481 return nil 482 }); err != nil { 483 return err 484 } 485 486 // this is a good time to do garbage collection 487 if err := s.db.RunValueLogGC(0.5); err != nil { 488 s.logger.Err(err).Msg("failed to run value log garbage collection") 489 } 490 491 return nil 492 } 493 494 func (s *storage) setPrunedHeight(height uint64) error { 495 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 496 prunedHeightValue := makeUint64Value(height) 497 498 return s.db.Update(func(txn *badger.Txn) error { 499 if err := txn.Set(prunedHeightKey, prunedHeightValue); err != nil { 500 return fmt.Errorf("failed to set pruned height value: %w", err) 501 } 502 503 return nil 504 }) 505 } 506 507 func (s *storage) GetPrunedHeight() (uint64, error) { 508 prunedHeightKey := makeGlobalStateKey(globalStatePrunedHeight) 509 var prunedHeight uint64 510 511 if err := s.db.View(func(txn *badger.Txn) error { 512 item, err := txn.Get(prunedHeightKey) 513 if err != nil { 514 return fmt.Errorf("failed to find pruned height entry: %w", err) 515 } 516 517 prunedHeight, err = getUint64Value(item) 518 if err != nil { 519 return fmt.Errorf("failed to retrieve pruned height value: %w", err) 520 } 521 522 return nil 523 }); err != nil { 524 return 0, err 525 } 526 527 return prunedHeight, nil 528 } 529 530 type deleteInfo struct { 531 cid cid.Cid 532 height uint64 533 deleteLatestHeightRecord bool 534 }