github.com/MetalBlockchain/metalgo@v1.11.9/snow/engine/avalanche/bootstrap/queue/jobs_test.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 "bytes" 8 "context" 9 "math" 10 "testing" 11 12 "github.com/prometheus/client_golang/prometheus" 13 "github.com/stretchr/testify/require" 14 15 "github.com/MetalBlockchain/metalgo/database" 16 "github.com/MetalBlockchain/metalgo/database/memdb" 17 "github.com/MetalBlockchain/metalgo/ids" 18 "github.com/MetalBlockchain/metalgo/snow/engine/common" 19 "github.com/MetalBlockchain/metalgo/snow/snowtest" 20 "github.com/MetalBlockchain/metalgo/utils/set" 21 ) 22 23 // Magic value that comes from the size in bytes of a serialized key-value bootstrap checkpoint in a database + 24 // the overhead of the key-value storage. 25 const bootstrapProgressCheckpointSize = 55 26 27 func testJob(t *testing.T, jobID ids.ID, executed *bool, parentID ids.ID, parentExecuted *bool) *TestJob { 28 return &TestJob{ 29 T: t, 30 IDF: func() ids.ID { 31 return jobID 32 }, 33 MissingDependenciesF: func(context.Context) (set.Set[ids.ID], error) { 34 if parentID != ids.Empty && !*parentExecuted { 35 return set.Of(parentID), nil 36 } 37 return set.Set[ids.ID]{}, nil 38 }, 39 HasMissingDependenciesF: func(context.Context) (bool, error) { 40 if parentID != ids.Empty && !*parentExecuted { 41 return true, nil 42 } 43 return false, nil 44 }, 45 ExecuteF: func(context.Context) error { 46 if executed != nil { 47 *executed = true 48 } 49 return nil 50 }, 51 BytesF: func() []byte { 52 return []byte{0} 53 }, 54 } 55 } 56 57 // Test that creating a new queue can be created and that it is initially empty. 58 func TestNew(t *testing.T) { 59 require := require.New(t) 60 61 parser := &TestParser{T: t} 62 db := memdb.New() 63 64 jobs, err := New(db, "", prometheus.NewRegistry()) 65 require.NoError(err) 66 require.NoError(jobs.SetParser(parser)) 67 68 dbSize, err := database.Size(db) 69 require.NoError(err) 70 require.Zero(dbSize) 71 } 72 73 // Test that a job can be added to a queue, and then the job can be executed 74 // from the queue after a shutdown. 75 func TestPushAndExecute(t *testing.T) { 76 require := require.New(t) 77 78 parser := &TestParser{T: t} 79 db := memdb.New() 80 81 jobs, err := New(db, "", prometheus.NewRegistry()) 82 require.NoError(err) 83 require.NoError(jobs.SetParser(parser)) 84 85 jobID := ids.GenerateTestID() 86 job := testJob(t, jobID, nil, ids.Empty, nil) 87 has, err := jobs.Has(jobID) 88 require.NoError(err) 89 require.False(has) 90 91 pushed, err := jobs.Push(context.Background(), job) 92 require.True(pushed) 93 require.NoError(err) 94 95 has, err = jobs.Has(jobID) 96 require.NoError(err) 97 require.True(has) 98 99 require.NoError(jobs.Commit()) 100 101 jobs, err = New(db, "", prometheus.NewRegistry()) 102 require.NoError(err) 103 require.NoError(jobs.SetParser(parser)) 104 105 has, err = jobs.Has(jobID) 106 require.NoError(err) 107 require.True(has) 108 109 hasNext, err := jobs.state.HasRunnableJob() 110 require.NoError(err) 111 require.True(hasNext) 112 113 parser.ParseF = func(_ context.Context, b []byte) (Job, error) { 114 require.Equal([]byte{0}, b) 115 return job, nil 116 } 117 118 snowCtx := snowtest.Context(t, snowtest.CChainID) 119 count, err := jobs.ExecuteAll(context.Background(), snowtest.ConsensusContext(snowCtx), &common.Halter{}, false) 120 require.NoError(err) 121 require.Equal(1, count) 122 123 has, err = jobs.Has(jobID) 124 require.NoError(err) 125 require.False(has) 126 127 hasNext, err = jobs.state.HasRunnableJob() 128 require.NoError(err) 129 require.False(hasNext) 130 131 dbSize, err := database.Size(db) 132 require.NoError(err) 133 require.Equal(bootstrapProgressCheckpointSize, dbSize) 134 } 135 136 // Test that executing a job will cause a dependent job to be placed on to the 137 // ready queue 138 func TestRemoveDependency(t *testing.T) { 139 require := require.New(t) 140 141 parser := &TestParser{T: t} 142 db := memdb.New() 143 144 jobs, err := New(db, "", prometheus.NewRegistry()) 145 require.NoError(err) 146 require.NoError(jobs.SetParser(parser)) 147 148 job0ID, executed0 := ids.GenerateTestID(), false 149 job1ID, executed1 := ids.GenerateTestID(), false 150 151 job0 := testJob(t, job0ID, &executed0, ids.Empty, nil) 152 job1 := testJob(t, job1ID, &executed1, job0ID, &executed0) 153 job1.BytesF = func() []byte { 154 return []byte{1} 155 } 156 157 pushed, err := jobs.Push(context.Background(), job1) 158 require.True(pushed) 159 require.NoError(err) 160 161 hasNext, err := jobs.state.HasRunnableJob() 162 require.NoError(err) 163 require.False(hasNext) 164 165 pushed, err = jobs.Push(context.Background(), job0) 166 require.True(pushed) 167 require.NoError(err) 168 169 hasNext, err = jobs.state.HasRunnableJob() 170 require.NoError(err) 171 require.True(hasNext) 172 173 parser.ParseF = func(_ context.Context, b []byte) (Job, error) { 174 switch { 175 case bytes.Equal(b, []byte{0}): 176 return job0, nil 177 case bytes.Equal(b, []byte{1}): 178 return job1, nil 179 default: 180 require.FailNow("Unknown job") 181 return nil, nil 182 } 183 } 184 185 snowCtx := snowtest.Context(t, snowtest.CChainID) 186 count, err := jobs.ExecuteAll(context.Background(), snowtest.ConsensusContext(snowCtx), &common.Halter{}, false) 187 require.NoError(err) 188 require.Equal(2, count) 189 require.True(executed0) 190 require.True(executed1) 191 192 hasNext, err = jobs.state.HasRunnableJob() 193 require.NoError(err) 194 require.False(hasNext) 195 196 dbSize, err := database.Size(db) 197 require.NoError(err) 198 require.Equal(bootstrapProgressCheckpointSize, dbSize) 199 } 200 201 // Test that a job that is ready to be executed can only be added once 202 func TestDuplicatedExecutablePush(t *testing.T) { 203 require := require.New(t) 204 205 db := memdb.New() 206 207 jobs, err := New(db, "", prometheus.NewRegistry()) 208 require.NoError(err) 209 210 jobID := ids.GenerateTestID() 211 job := testJob(t, jobID, nil, ids.Empty, nil) 212 213 pushed, err := jobs.Push(context.Background(), job) 214 require.True(pushed) 215 require.NoError(err) 216 217 pushed, err = jobs.Push(context.Background(), job) 218 require.False(pushed) 219 require.NoError(err) 220 221 require.NoError(jobs.Commit()) 222 223 jobs, err = New(db, "", prometheus.NewRegistry()) 224 require.NoError(err) 225 226 pushed, err = jobs.Push(context.Background(), job) 227 require.False(pushed) 228 require.NoError(err) 229 } 230 231 // Test that a job that isn't ready to be executed can only be added once 232 func TestDuplicatedNotExecutablePush(t *testing.T) { 233 require := require.New(t) 234 235 db := memdb.New() 236 237 jobs, err := New(db, "", prometheus.NewRegistry()) 238 require.NoError(err) 239 240 job0ID, executed0 := ids.GenerateTestID(), false 241 job1ID := ids.GenerateTestID() 242 job1 := testJob(t, job1ID, nil, job0ID, &executed0) 243 244 pushed, err := jobs.Push(context.Background(), job1) 245 require.True(pushed) 246 require.NoError(err) 247 248 pushed, err = jobs.Push(context.Background(), job1) 249 require.False(pushed) 250 require.NoError(err) 251 252 require.NoError(jobs.Commit()) 253 254 jobs, err = New(db, "", prometheus.NewRegistry()) 255 require.NoError(err) 256 257 pushed, err = jobs.Push(context.Background(), job1) 258 require.False(pushed) 259 require.NoError(err) 260 } 261 262 func TestMissingJobs(t *testing.T) { 263 require := require.New(t) 264 265 parser := &TestParser{T: t} 266 db := memdb.New() 267 268 jobs, err := NewWithMissing(db, "", prometheus.NewRegistry()) 269 require.NoError(err) 270 require.NoError(jobs.SetParser(context.Background(), parser)) 271 272 job0ID := ids.GenerateTestID() 273 job1ID := ids.GenerateTestID() 274 275 jobs.AddMissingID(job0ID) 276 jobs.AddMissingID(job1ID) 277 278 require.NoError(jobs.Commit()) 279 280 numMissingIDs := jobs.NumMissingIDs() 281 require.Equal(2, numMissingIDs) 282 283 missingIDSet := set.Of(jobs.MissingIDs()...) 284 285 containsJob0ID := missingIDSet.Contains(job0ID) 286 require.True(containsJob0ID) 287 288 containsJob1ID := missingIDSet.Contains(job1ID) 289 require.True(containsJob1ID) 290 291 jobs.RemoveMissingID(job1ID) 292 293 require.NoError(jobs.Commit()) 294 295 jobs, err = NewWithMissing(db, "", prometheus.NewRegistry()) 296 require.NoError(err) 297 require.NoError(jobs.SetParser(context.Background(), parser)) 298 299 missingIDSet = set.Of(jobs.MissingIDs()...) 300 301 containsJob0ID = missingIDSet.Contains(job0ID) 302 require.True(containsJob0ID) 303 304 containsJob1ID = missingIDSet.Contains(job1ID) 305 require.False(containsJob1ID) 306 } 307 308 func TestHandleJobWithMissingDependencyOnRunnableStack(t *testing.T) { 309 require := require.New(t) 310 311 parser := &TestParser{T: t} 312 db := memdb.New() 313 314 jobs, err := NewWithMissing(db, "", prometheus.NewRegistry()) 315 require.NoError(err) 316 require.NoError(jobs.SetParser(context.Background(), parser)) 317 318 job0ID, executed0 := ids.GenerateTestID(), false 319 job1ID, executed1 := ids.GenerateTestID(), false 320 job0 := testJob(t, job0ID, &executed0, ids.Empty, nil) 321 job1 := testJob(t, job1ID, &executed1, job0ID, &executed0) 322 323 // job1 fails to execute the first time due to a closed database 324 job1.ExecuteF = func(context.Context) error { 325 return database.ErrClosed 326 } 327 job1.BytesF = func() []byte { 328 return []byte{1} 329 } 330 331 pushed, err := jobs.Push(context.Background(), job1) 332 require.True(pushed) 333 require.NoError(err) 334 335 hasNext, err := jobs.state.HasRunnableJob() 336 require.NoError(err) 337 require.False(hasNext) 338 339 pushed, err = jobs.Push(context.Background(), job0) 340 require.True(pushed) 341 require.NoError(err) 342 343 hasNext, err = jobs.state.HasRunnableJob() 344 require.NoError(err) 345 require.True(hasNext) 346 347 parser.ParseF = func(_ context.Context, b []byte) (Job, error) { 348 switch { 349 case bytes.Equal(b, []byte{0}): 350 return job0, nil 351 case bytes.Equal(b, []byte{1}): 352 return job1, nil 353 default: 354 require.FailNow("Unknown job") 355 return nil, nil 356 } 357 } 358 359 snowCtx := snowtest.Context(t, snowtest.CChainID) 360 _, err = jobs.ExecuteAll(context.Background(), snowtest.ConsensusContext(snowCtx), &common.Halter{}, false) 361 // Assert that the database closed error on job1 causes ExecuteAll 362 // to fail in the middle of execution. 363 require.ErrorIs(err, database.ErrClosed) 364 require.True(executed0) 365 require.False(executed1) 366 367 executed0 = false 368 job1.ExecuteF = func(context.Context) error { 369 executed1 = true // job1 succeeds the second time 370 return nil 371 } 372 373 // Create jobs queue from the same database and ensure that the jobs queue 374 // recovers correctly. 375 jobs, err = NewWithMissing(db, "", prometheus.NewRegistry()) 376 require.NoError(err) 377 require.NoError(jobs.SetParser(context.Background(), parser)) 378 379 missingIDs := jobs.MissingIDs() 380 require.Len(missingIDs, 1) 381 382 require.Equal(missingIDs[0], job0.ID()) 383 384 pushed, err = jobs.Push(context.Background(), job0) 385 require.NoError(err) 386 require.True(pushed) 387 388 hasNext, err = jobs.state.HasRunnableJob() 389 require.NoError(err) 390 require.True(hasNext) 391 392 count, err := jobs.ExecuteAll(context.Background(), snowtest.ConsensusContext(snowCtx), &common.Halter{}, false) 393 require.NoError(err) 394 require.Equal(2, count) 395 require.True(executed1) 396 } 397 398 func TestInitializeNumJobs(t *testing.T) { 399 require := require.New(t) 400 401 parser := &TestParser{T: t} 402 db := memdb.New() 403 404 jobs, err := NewWithMissing(db, "", prometheus.NewRegistry()) 405 require.NoError(err) 406 require.NoError(jobs.SetParser(context.Background(), parser)) 407 408 job0ID := ids.GenerateTestID() 409 job1ID := ids.GenerateTestID() 410 411 job0 := &TestJob{ 412 T: t, 413 414 IDF: func() ids.ID { 415 return job0ID 416 }, 417 MissingDependenciesF: func(context.Context) (set.Set[ids.ID], error) { 418 return nil, nil 419 }, 420 HasMissingDependenciesF: func(context.Context) (bool, error) { 421 return false, nil 422 }, 423 BytesF: func() []byte { 424 return []byte{0} 425 }, 426 } 427 job1 := &TestJob{ 428 T: t, 429 430 IDF: func() ids.ID { 431 return job1ID 432 }, 433 MissingDependenciesF: func(context.Context) (set.Set[ids.ID], error) { 434 return nil, nil 435 }, 436 HasMissingDependenciesF: func(context.Context) (bool, error) { 437 return false, nil 438 }, 439 BytesF: func() []byte { 440 return []byte{1} 441 }, 442 } 443 444 pushed, err := jobs.Push(context.Background(), job0) 445 require.True(pushed) 446 require.NoError(err) 447 require.Equal(uint64(1), jobs.state.numJobs) 448 449 pushed, err = jobs.Push(context.Background(), job1) 450 require.True(pushed) 451 require.NoError(err) 452 require.Equal(uint64(2), jobs.state.numJobs) 453 454 require.NoError(jobs.Commit()) 455 require.NoError(database.Clear(jobs.state.metadataDB, math.MaxInt)) 456 require.NoError(jobs.Commit()) 457 458 jobs, err = NewWithMissing(db, "", prometheus.NewRegistry()) 459 require.NoError(err) 460 require.Equal(uint64(2), jobs.state.numJobs) 461 } 462 463 func TestClearAll(t *testing.T) { 464 require := require.New(t) 465 466 parser := &TestParser{T: t} 467 db := memdb.New() 468 469 jobs, err := NewWithMissing(db, "", prometheus.NewRegistry()) 470 require.NoError(err) 471 require.NoError(jobs.SetParser(context.Background(), parser)) 472 job0ID, executed0 := ids.GenerateTestID(), false 473 job1ID, executed1 := ids.GenerateTestID(), false 474 job0 := testJob(t, job0ID, &executed0, ids.Empty, nil) 475 job1 := testJob(t, job1ID, &executed1, job0ID, &executed0) 476 job1.BytesF = func() []byte { 477 return []byte{1} 478 } 479 480 pushed, err := jobs.Push(context.Background(), job0) 481 require.NoError(err) 482 require.True(pushed) 483 484 pushed, err = jobs.Push(context.Background(), job1) 485 require.True(pushed) 486 require.NoError(err) 487 488 parser.ParseF = func(_ context.Context, b []byte) (Job, error) { 489 switch { 490 case bytes.Equal(b, []byte{0}): 491 return job0, nil 492 case bytes.Equal(b, []byte{1}): 493 return job1, nil 494 default: 495 require.FailNow("Unknown job") 496 return nil, nil 497 } 498 } 499 500 require.NoError(jobs.Clear()) 501 hasJob0, err := jobs.Has(job0.ID()) 502 require.NoError(err) 503 require.False(hasJob0) 504 hasJob1, err := jobs.Has(job1.ID()) 505 require.NoError(err) 506 require.False(hasJob1) 507 }