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  }