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  }