github.com/MetalBlockchain/metalgo@v1.11.9/x/sync/workheap_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 sync
     5  
     6  import (
     7  	"bytes"
     8  	"math/rand"
     9  	"slices"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/MetalBlockchain/metalgo/ids"
    16  	"github.com/MetalBlockchain/metalgo/utils/maybe"
    17  )
    18  
    19  // Tests Insert and GetWork
    20  func Test_WorkHeap_Insert_GetWork(t *testing.T) {
    21  	require := require.New(t)
    22  	h := newWorkHeap()
    23  
    24  	lowPriorityItem := &workItem{
    25  		start:       maybe.Some([]byte{4}),
    26  		end:         maybe.Some([]byte{5}),
    27  		priority:    lowPriority,
    28  		localRootID: ids.GenerateTestID(),
    29  	}
    30  	mediumPriorityItem := &workItem{
    31  		start:       maybe.Some([]byte{0}),
    32  		end:         maybe.Some([]byte{1}),
    33  		priority:    medPriority,
    34  		localRootID: ids.GenerateTestID(),
    35  	}
    36  	highPriorityItem := &workItem{
    37  		start:       maybe.Some([]byte{2}),
    38  		end:         maybe.Some([]byte{3}),
    39  		priority:    highPriority,
    40  		localRootID: ids.GenerateTestID(),
    41  	}
    42  	h.Insert(highPriorityItem)
    43  	h.Insert(mediumPriorityItem)
    44  	h.Insert(lowPriorityItem)
    45  	require.Equal(3, h.Len())
    46  
    47  	// Ensure [sortedItems] is in right order.
    48  	got := []*workItem{}
    49  	h.sortedItems.Ascend(
    50  		func(i *workItem) bool {
    51  			got = append(got, i)
    52  			return true
    53  		},
    54  	)
    55  	require.Equal(
    56  		[]*workItem{mediumPriorityItem, highPriorityItem, lowPriorityItem},
    57  		got,
    58  	)
    59  
    60  	// Ensure priorities are in right order.
    61  	gotItem := h.GetWork()
    62  	require.Equal(highPriorityItem, gotItem)
    63  	gotItem = h.GetWork()
    64  	require.Equal(mediumPriorityItem, gotItem)
    65  	gotItem = h.GetWork()
    66  	require.Equal(lowPriorityItem, gotItem)
    67  	gotItem = h.GetWork()
    68  	require.Nil(gotItem)
    69  
    70  	require.Zero(h.Len())
    71  }
    72  
    73  func Test_WorkHeap_remove(t *testing.T) {
    74  	require := require.New(t)
    75  
    76  	h := newWorkHeap()
    77  
    78  	lowPriorityItem := &workItem{
    79  		start:       maybe.Some([]byte{0}),
    80  		end:         maybe.Some([]byte{1}),
    81  		priority:    lowPriority,
    82  		localRootID: ids.GenerateTestID(),
    83  	}
    84  
    85  	mediumPriorityItem := &workItem{
    86  		start:       maybe.Some([]byte{2}),
    87  		end:         maybe.Some([]byte{3}),
    88  		priority:    medPriority,
    89  		localRootID: ids.GenerateTestID(),
    90  	}
    91  
    92  	highPriorityItem := &workItem{
    93  		start:       maybe.Some([]byte{4}),
    94  		end:         maybe.Some([]byte{5}),
    95  		priority:    highPriority,
    96  		localRootID: ids.GenerateTestID(),
    97  	}
    98  
    99  	h.Insert(lowPriorityItem)
   100  
   101  	wrappedLowPriorityItem, ok := h.innerHeap.Peek()
   102  	require.True(ok)
   103  	h.remove(wrappedLowPriorityItem)
   104  
   105  	require.Zero(h.Len())
   106  	require.Zero(h.sortedItems.Len())
   107  
   108  	h.Insert(lowPriorityItem)
   109  	h.Insert(mediumPriorityItem)
   110  	h.Insert(highPriorityItem)
   111  
   112  	wrappedhighPriorityItem, ok := h.innerHeap.Peek()
   113  	require.True(ok)
   114  	require.Equal(highPriorityItem, wrappedhighPriorityItem)
   115  	h.remove(wrappedhighPriorityItem)
   116  	require.Equal(2, h.Len())
   117  	require.Equal(2, h.sortedItems.Len())
   118  	got, ok := h.innerHeap.Peek()
   119  	require.True(ok)
   120  	require.Equal(mediumPriorityItem, got)
   121  
   122  	wrappedMediumPriorityItem, ok := h.innerHeap.Peek()
   123  	require.True(ok)
   124  	require.Equal(mediumPriorityItem, wrappedMediumPriorityItem)
   125  	h.remove(wrappedMediumPriorityItem)
   126  	require.Equal(1, h.Len())
   127  	require.Equal(1, h.sortedItems.Len())
   128  	got, ok = h.innerHeap.Peek()
   129  	require.True(ok)
   130  	require.Equal(lowPriorityItem, got)
   131  
   132  	wrappedLowPriorityItem, ok = h.innerHeap.Peek()
   133  	require.True(ok)
   134  	require.Equal(lowPriorityItem, wrappedLowPriorityItem)
   135  	h.remove(wrappedLowPriorityItem)
   136  	require.Zero(h.Len())
   137  	require.Zero(h.sortedItems.Len())
   138  }
   139  
   140  func Test_WorkHeap_Merge_Insert(t *testing.T) {
   141  	// merge with range before
   142  	syncHeap := newWorkHeap()
   143  
   144  	syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})})
   145  	require.Equal(t, 1, syncHeap.Len())
   146  
   147  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Some([]byte{192})})
   148  	require.Equal(t, 2, syncHeap.Len())
   149  
   150  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{193}), end: maybe.Nothing[[]byte]()})
   151  	require.Equal(t, 3, syncHeap.Len())
   152  
   153  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{63}), end: maybe.Some([]byte{126}), priority: lowPriority})
   154  	require.Equal(t, 3, syncHeap.Len())
   155  
   156  	// merge with range after
   157  	syncHeap = newWorkHeap()
   158  
   159  	syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})})
   160  	require.Equal(t, 1, syncHeap.Len())
   161  
   162  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Some([]byte{192})})
   163  	require.Equal(t, 2, syncHeap.Len())
   164  
   165  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{193}), end: maybe.Nothing[[]byte]()})
   166  	require.Equal(t, 3, syncHeap.Len())
   167  
   168  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{64}), end: maybe.Some([]byte{127}), priority: lowPriority})
   169  	require.Equal(t, 3, syncHeap.Len())
   170  
   171  	// merge both sides at the same time
   172  	syncHeap = newWorkHeap()
   173  
   174  	syncHeap.MergeInsert(&workItem{start: maybe.Nothing[[]byte](), end: maybe.Some([]byte{63})})
   175  	require.Equal(t, 1, syncHeap.Len())
   176  
   177  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{127}), end: maybe.Nothing[[]byte]()})
   178  	require.Equal(t, 2, syncHeap.Len())
   179  
   180  	syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{63}), end: maybe.Some([]byte{127}), priority: lowPriority})
   181  	require.Equal(t, 1, syncHeap.Len())
   182  }
   183  
   184  func TestWorkHeapMergeInsertRandom(t *testing.T) {
   185  	var (
   186  		require   = require.New(t)
   187  		seed      = time.Now().UnixNano()
   188  		rand      = rand.New(rand.NewSource(seed)) // #nosec G404
   189  		numRanges = 1_000
   190  		bounds    = [][]byte{}
   191  		rootID    = ids.GenerateTestID()
   192  	)
   193  	t.Logf("seed: %d", seed)
   194  
   195  	// Create start and end bounds
   196  	for i := 0; i < numRanges; i++ {
   197  		bound := make([]byte, 32)
   198  		_, _ = rand.Read(bound)
   199  		bounds = append(bounds, bound)
   200  	}
   201  	slices.SortFunc(bounds, bytes.Compare)
   202  
   203  	// Note that start < end for all ranges.
   204  	// It is possible but extremely unlikely that
   205  	// two elements of [bounds] are equal.
   206  	ranges := []workItem{}
   207  	for i := 0; i < numRanges/2; i++ {
   208  		start := bounds[i*2]
   209  		end := bounds[i*2+1]
   210  		ranges = append(ranges, workItem{
   211  			start:    maybe.Some(start),
   212  			end:      maybe.Some(end),
   213  			priority: lowPriority,
   214  			// Note they all share the same root ID.
   215  			localRootID: rootID,
   216  		})
   217  	}
   218  	// Set beginning of first range to Nothing.
   219  	ranges[0].start = maybe.Nothing[[]byte]()
   220  	// Set end of last range to Nothing.
   221  	ranges[len(ranges)-1].end = maybe.Nothing[[]byte]()
   222  
   223  	setup := func() *workHeap {
   224  		// Insert all the ranges into the heap.
   225  		h := newWorkHeap()
   226  		for i, r := range ranges {
   227  			require.Equal(i, h.Len())
   228  			rCopy := r
   229  			h.MergeInsert(&rCopy)
   230  		}
   231  		return h
   232  	}
   233  
   234  	{
   235  		// Case 1: Merging an item with the range before and after
   236  		h := setup()
   237  		// Keep merging ranges until there's only one range left.
   238  		for i := 0; i < len(ranges)-1; i++ {
   239  			// Merge ranges[i] with ranges[i+1]
   240  			h.MergeInsert(&workItem{
   241  				start:       ranges[i].end,
   242  				end:         ranges[i+1].start,
   243  				priority:    lowPriority,
   244  				localRootID: rootID,
   245  			})
   246  			require.Equal(len(ranges)-i-1, h.Len())
   247  		}
   248  		got := h.GetWork()
   249  		require.True(got.start.IsNothing())
   250  		require.True(got.end.IsNothing())
   251  	}
   252  
   253  	{
   254  		// Case 2: Merging an item with the range before
   255  		h := setup()
   256  		for i := 0; i < len(ranges)-1; i++ {
   257  			// Extend end of ranges[i]
   258  			newEnd := slices.Clone(ranges[i].end.Value())
   259  			newEnd = append(newEnd, 0)
   260  			h.MergeInsert(&workItem{
   261  				start:       ranges[i].end,
   262  				end:         maybe.Some(newEnd),
   263  				priority:    lowPriority,
   264  				localRootID: rootID,
   265  			})
   266  
   267  			// Shouldn't cause number of elements to change
   268  			require.Equal(len(ranges), h.Len())
   269  
   270  			start := ranges[i].start
   271  			if i == 0 {
   272  				start = maybe.Nothing[[]byte]()
   273  			}
   274  			// Make sure end is updated
   275  			got, ok := h.sortedItems.Get(&workItem{
   276  				start: start,
   277  			})
   278  			require.True(ok)
   279  			require.Equal(newEnd, got.end.Value())
   280  		}
   281  	}
   282  
   283  	{
   284  		// Case 3: Merging an item with the range after
   285  		h := setup()
   286  		for i := 1; i < len(ranges); i++ {
   287  			// Extend start of ranges[i]
   288  			newStartBytes := slices.Clone(ranges[i].start.Value())
   289  			newStartBytes = newStartBytes[:len(newStartBytes)-1]
   290  			newStart := maybe.Some(newStartBytes)
   291  
   292  			h.MergeInsert(&workItem{
   293  				start:       newStart,
   294  				end:         ranges[i].start,
   295  				priority:    lowPriority,
   296  				localRootID: rootID,
   297  			})
   298  
   299  			// Shouldn't cause number of elements to change
   300  			require.Equal(len(ranges), h.Len())
   301  
   302  			// Make sure start is updated
   303  			got, ok := h.sortedItems.Get(&workItem{
   304  				start: newStart,
   305  			})
   306  			require.True(ok)
   307  			require.Equal(newStartBytes, got.start.Value())
   308  		}
   309  	}
   310  }