github.com/MetalBlockchain/metalgo@v1.11.9/snow/engine/avalanche/bootstrap/queue/state.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package queue
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"github.com/prometheus/client_golang/prometheus"
    12  
    13  	"github.com/MetalBlockchain/metalgo/cache"
    14  	"github.com/MetalBlockchain/metalgo/cache/metercacher"
    15  	"github.com/MetalBlockchain/metalgo/database"
    16  	"github.com/MetalBlockchain/metalgo/database/linkeddb"
    17  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    18  	"github.com/MetalBlockchain/metalgo/ids"
    19  	"github.com/MetalBlockchain/metalgo/utils/metric"
    20  	"github.com/MetalBlockchain/metalgo/utils/set"
    21  )
    22  
    23  const (
    24  	dependentsCacheSize = 1024
    25  	jobsCacheSize       = 2048
    26  )
    27  
    28  var (
    29  	runnableJobIDsPrefix = []byte("runnable")
    30  	jobsPrefix           = []byte("jobs")
    31  	dependenciesPrefix   = []byte("dependencies")
    32  	missingJobIDsPrefix  = []byte("missing job IDs")
    33  	metadataPrefix       = []byte("metadata")
    34  	numJobsKey           = []byte("numJobs")
    35  )
    36  
    37  type state struct {
    38  	parser         Parser
    39  	runnableJobIDs linkeddb.LinkedDB
    40  	cachingEnabled bool
    41  	jobsCache      cache.Cacher[ids.ID, Job]
    42  	jobsDB         database.Database
    43  	// Should be prefixed with the jobID that we are attempting to find the
    44  	// dependencies of. This prefixdb.Database should then be wrapped in a
    45  	// linkeddb.LinkedDB to read the dependencies.
    46  	dependenciesDB database.Database
    47  	// This is a cache that tracks LinkedDB iterators that have recently been
    48  	// made.
    49  	dependentsCache cache.Cacher[ids.ID, linkeddb.LinkedDB]
    50  	missingJobIDs   linkeddb.LinkedDB
    51  	// This tracks the summary values of this state. Currently, this only
    52  	// contains the last known checkpoint of how many jobs are currently in the
    53  	// queue to execute.
    54  	metadataDB database.Database
    55  	// This caches the number of jobs that are currently in the queue to
    56  	// execute.
    57  	numJobs uint64
    58  }
    59  
    60  func newState(
    61  	db database.Database,
    62  	metricsNamespace string,
    63  	metricsRegisterer prometheus.Registerer,
    64  ) (*state, error) {
    65  	jobsCacheMetricsNamespace := metric.AppendNamespace(metricsNamespace, "jobs_cache")
    66  	jobsCache, err := metercacher.New[ids.ID, Job](
    67  		jobsCacheMetricsNamespace,
    68  		metricsRegisterer,
    69  		&cache.LRU[ids.ID, Job]{
    70  			Size: jobsCacheSize,
    71  		},
    72  	)
    73  	if err != nil {
    74  		return nil, fmt.Errorf("couldn't create metered cache: %w", err)
    75  	}
    76  
    77  	metadataDB := prefixdb.New(metadataPrefix, db)
    78  	jobs := prefixdb.New(jobsPrefix, db)
    79  	numJobs, err := getNumJobs(metadataDB, jobs)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("couldn't initialize pending jobs: %w", err)
    82  	}
    83  	return &state{
    84  		runnableJobIDs:  linkeddb.NewDefault(prefixdb.New(runnableJobIDsPrefix, db)),
    85  		cachingEnabled:  true,
    86  		jobsCache:       jobsCache,
    87  		jobsDB:          jobs,
    88  		dependenciesDB:  prefixdb.New(dependenciesPrefix, db),
    89  		dependentsCache: &cache.LRU[ids.ID, linkeddb.LinkedDB]{Size: dependentsCacheSize},
    90  		missingJobIDs:   linkeddb.NewDefault(prefixdb.New(missingJobIDsPrefix, db)),
    91  		metadataDB:      metadataDB,
    92  		numJobs:         numJobs,
    93  	}, nil
    94  }
    95  
    96  func getNumJobs(d database.Database, jobs database.Iteratee) (uint64, error) {
    97  	numJobs, err := database.GetUInt64(d, numJobsKey)
    98  	if err == database.ErrNotFound {
    99  		// If we don't have a checkpoint, we need to initialize it.
   100  		count, err := database.Count(jobs)
   101  		return uint64(count), err
   102  	}
   103  	return numJobs, err
   104  }
   105  
   106  func (s *state) Clear() error {
   107  	var (
   108  		runJobsIter  = s.runnableJobIDs.NewIterator()
   109  		jobsIter     = s.jobsDB.NewIterator()
   110  		depsIter     = s.dependenciesDB.NewIterator()
   111  		missJobsIter = s.missingJobIDs.NewIterator()
   112  	)
   113  	defer func() {
   114  		runJobsIter.Release()
   115  		jobsIter.Release()
   116  		depsIter.Release()
   117  		missJobsIter.Release()
   118  	}()
   119  
   120  	// clear runnableJobIDs
   121  	for runJobsIter.Next() {
   122  		if err := s.runnableJobIDs.Delete(runJobsIter.Key()); err != nil {
   123  			return err
   124  		}
   125  	}
   126  
   127  	// clear jobs
   128  	s.jobsCache.Flush()
   129  	for jobsIter.Next() {
   130  		if err := s.jobsDB.Delete(jobsIter.Key()); err != nil {
   131  			return err
   132  		}
   133  	}
   134  
   135  	// clear dependencies
   136  	s.dependentsCache.Flush()
   137  	for depsIter.Next() {
   138  		if err := s.dependenciesDB.Delete(depsIter.Key()); err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	// clear missing jobs IDs
   144  	for missJobsIter.Next() {
   145  		if err := s.missingJobIDs.Delete(missJobsIter.Key()); err != nil {
   146  			return err
   147  		}
   148  	}
   149  
   150  	// clear number of pending jobs
   151  	s.numJobs = 0
   152  	if err := database.PutUInt64(s.metadataDB, numJobsKey, s.numJobs); err != nil {
   153  		return err
   154  	}
   155  
   156  	return errors.Join(
   157  		runJobsIter.Error(),
   158  		jobsIter.Error(),
   159  		depsIter.Error(),
   160  		missJobsIter.Error(),
   161  	)
   162  }
   163  
   164  // AddRunnableJob adds [jobID] to the runnable queue
   165  func (s *state) AddRunnableJob(jobID ids.ID) error {
   166  	return s.runnableJobIDs.Put(jobID[:], nil)
   167  }
   168  
   169  // HasRunnableJob returns true if there is a job that can be run on the queue
   170  func (s *state) HasRunnableJob() (bool, error) {
   171  	isEmpty, err := s.runnableJobIDs.IsEmpty()
   172  	return !isEmpty, err
   173  }
   174  
   175  // RemoveRunnableJob fetches and deletes the next job from the runnable queue
   176  func (s *state) RemoveRunnableJob(ctx context.Context) (Job, error) {
   177  	jobIDBytes, err := s.runnableJobIDs.HeadKey()
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	if err := s.runnableJobIDs.Delete(jobIDBytes); err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	jobID, err := ids.ToID(jobIDBytes)
   186  	if err != nil {
   187  		return nil, fmt.Errorf("couldn't convert job ID bytes to job ID: %w", err)
   188  	}
   189  	job, err := s.GetJob(ctx, jobID)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	if err := s.jobsDB.Delete(jobIDBytes); err != nil {
   195  		return job, err
   196  	}
   197  
   198  	// Guard rail to make sure we don't underflow.
   199  	if s.numJobs == 0 {
   200  		return job, nil
   201  	}
   202  	s.numJobs--
   203  
   204  	return job, database.PutUInt64(s.metadataDB, numJobsKey, s.numJobs)
   205  }
   206  
   207  // PutJob adds the job to the queue
   208  func (s *state) PutJob(job Job) error {
   209  	id := job.ID()
   210  	if s.cachingEnabled {
   211  		s.jobsCache.Put(id, job)
   212  	}
   213  
   214  	if err := s.jobsDB.Put(id[:], job.Bytes()); err != nil {
   215  		return err
   216  	}
   217  
   218  	s.numJobs++
   219  	return database.PutUInt64(s.metadataDB, numJobsKey, s.numJobs)
   220  }
   221  
   222  // HasJob returns true if the job [id] is in the queue
   223  func (s *state) HasJob(id ids.ID) (bool, error) {
   224  	if s.cachingEnabled {
   225  		if _, exists := s.jobsCache.Get(id); exists {
   226  			return true, nil
   227  		}
   228  	}
   229  	return s.jobsDB.Has(id[:])
   230  }
   231  
   232  // GetJob returns the job [id]
   233  func (s *state) GetJob(ctx context.Context, id ids.ID) (Job, error) {
   234  	if s.cachingEnabled {
   235  		if job, exists := s.jobsCache.Get(id); exists {
   236  			return job, nil
   237  		}
   238  	}
   239  	jobBytes, err := s.jobsDB.Get(id[:])
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	job, err := s.parser.Parse(ctx, jobBytes)
   244  	if err == nil && s.cachingEnabled {
   245  		s.jobsCache.Put(id, job)
   246  	}
   247  	return job, err
   248  }
   249  
   250  // AddDependency adds [dependent] as blocking on [dependency] being completed
   251  func (s *state) AddDependency(dependency, dependent ids.ID) error {
   252  	dependentsDB := s.getDependentsDB(dependency)
   253  	return dependentsDB.Put(dependent[:], nil)
   254  }
   255  
   256  // RemoveDependencies removes the set of IDs that are blocking on the completion
   257  // of [dependency] from the database and returns them.
   258  func (s *state) RemoveDependencies(dependency ids.ID) ([]ids.ID, error) {
   259  	dependentsDB := s.getDependentsDB(dependency)
   260  	iterator := dependentsDB.NewIterator()
   261  	defer iterator.Release()
   262  
   263  	dependents := []ids.ID(nil)
   264  	for iterator.Next() {
   265  		dependentKey := iterator.Key()
   266  		if err := dependentsDB.Delete(dependentKey); err != nil {
   267  			return nil, err
   268  		}
   269  		dependent, err := ids.ToID(dependentKey)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		dependents = append(dependents, dependent)
   274  	}
   275  	return dependents, iterator.Error()
   276  }
   277  
   278  func (s *state) DisableCaching() {
   279  	s.dependentsCache.Flush()
   280  	s.jobsCache.Flush()
   281  	s.cachingEnabled = false
   282  }
   283  
   284  func (s *state) AddMissingJobIDs(missingIDs set.Set[ids.ID]) error {
   285  	for missingID := range missingIDs {
   286  		missingID := missingID
   287  		if err := s.missingJobIDs.Put(missingID[:], nil); err != nil {
   288  			return err
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func (s *state) RemoveMissingJobIDs(missingIDs set.Set[ids.ID]) error {
   295  	for missingID := range missingIDs {
   296  		missingID := missingID
   297  		if err := s.missingJobIDs.Delete(missingID[:]); err != nil {
   298  			return err
   299  		}
   300  	}
   301  	return nil
   302  }
   303  
   304  func (s *state) MissingJobIDs() ([]ids.ID, error) {
   305  	iterator := s.missingJobIDs.NewIterator()
   306  	defer iterator.Release()
   307  
   308  	missingIDs := []ids.ID(nil)
   309  	for iterator.Next() {
   310  		missingID, err := ids.ToID(iterator.Key())
   311  		if err != nil {
   312  			return nil, err
   313  		}
   314  		missingIDs = append(missingIDs, missingID)
   315  	}
   316  	return missingIDs, iterator.Error()
   317  }
   318  
   319  func (s *state) getDependentsDB(dependency ids.ID) linkeddb.LinkedDB {
   320  	if s.cachingEnabled {
   321  		if dependentsDB, ok := s.dependentsCache.Get(dependency); ok {
   322  			return dependentsDB
   323  		}
   324  	}
   325  	dependencyDB := prefixdb.New(dependency[:], s.dependenciesDB)
   326  	dependentsDB := linkeddb.NewDefault(dependencyDB)
   327  	if s.cachingEnabled {
   328  		s.dependentsCache.Put(dependency, dependentsDB)
   329  	}
   330  	return dependentsDB
   331  }