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 }