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  }