github.com/koko1123/flow-go-1@v0.29.6/ledger/complete/compactor_test.go (about)

     1  package complete
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"reflect"
     8  	"regexp"
     9  	"sync"
    10  	"testing"
    11  	"time"
    12  
    13  	prometheusWAL "github.com/m4ksio/wal/wal"
    14  	"github.com/rs/zerolog"
    15  	"github.com/rs/zerolog/log"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	"go.uber.org/atomic"
    19  
    20  	"github.com/koko1123/flow-go-1/ledger"
    21  	"github.com/koko1123/flow-go-1/ledger/common/testutils"
    22  	"github.com/koko1123/flow-go-1/ledger/complete/mtrie"
    23  	"github.com/koko1123/flow-go-1/ledger/complete/mtrie/trie"
    24  	realWAL "github.com/koko1123/flow-go-1/ledger/complete/wal"
    25  	"github.com/koko1123/flow-go-1/module/metrics"
    26  	"github.com/koko1123/flow-go-1/utils/unittest"
    27  )
    28  
    29  // slow down updating the ledger, because running too fast would cause the previous checkpoint
    30  // to not finish and get delayed, and this might cause tests to stuck
    31  const LedgerUpdateDelay = time.Millisecond * 500
    32  
    33  // Compactor observer that waits until it gets notified of a
    34  // latest checkpoint larger than fromBound
    35  type CompactorObserver struct {
    36  	fromBound int
    37  	done      chan struct{}
    38  }
    39  
    40  func (co *CompactorObserver) OnNext(val interface{}) {
    41  	res, ok := val.(int)
    42  	if ok {
    43  		newCheckpoint := res
    44  		fmt.Printf("Compactor observer received checkpoint num %d, will stop when checkpoint num (%v) >= %v\n",
    45  			newCheckpoint, newCheckpoint, co.fromBound)
    46  		if newCheckpoint >= co.fromBound {
    47  			co.done <- struct{}{}
    48  		}
    49  	}
    50  	fmt.Println("Compactor observer res:", res)
    51  }
    52  
    53  func (co *CompactorObserver) OnError(err error) {}
    54  func (co *CompactorObserver) OnComplete() {
    55  	close(co.done)
    56  }
    57  
    58  // TestCompactorCreation tests creation of WAL segments and checkpoints, and
    59  // checks if the rebuilt ledger state matches previous ledger state.
    60  func TestCompactorCreation(t *testing.T) {
    61  	const (
    62  		numInsPerStep      = 2
    63  		pathByteSize       = 32
    64  		minPayloadByteSize = 2 << 15 // 64  KB
    65  		maxPayloadByteSize = 2 << 16 // 128 KB
    66  		size               = 10
    67  		checkpointDistance = 3
    68  		checkpointsToKeep  = 1
    69  		forestCapacity     = size * 10
    70  		segmentSize        = 32 * 1024 // 32 KB
    71  	)
    72  
    73  	metricsCollector := &metrics.NoopCollector{}
    74  
    75  	unittest.RunWithTempDir(t, func(dir string) {
    76  
    77  		var l *Ledger
    78  
    79  		// saved data after updates
    80  		savedData := make(map[ledger.RootHash]map[string]*ledger.Payload)
    81  
    82  		t.Run("creates checkpoints", func(t *testing.T) {
    83  
    84  			wal, err := realWAL.NewDiskWAL(unittest.Logger(), nil, metrics.NewNoopCollector(), dir, forestCapacity, pathByteSize, segmentSize)
    85  			require.NoError(t, err)
    86  
    87  			l, err = NewLedger(wal, size*10, metricsCollector, unittest.Logger(), DefaultPathFinderVersion)
    88  			require.NoError(t, err)
    89  
    90  			// WAL segments are 32kB, so here we generate 2 keys 64kB each, times `size`
    91  			// so we should get at least `size` segments
    92  
    93  			compactor, err := NewCompactor(l, wal, unittest.Logger(), forestCapacity, checkpointDistance, checkpointsToKeep, atomic.NewBool(false))
    94  			require.NoError(t, err)
    95  
    96  			co := CompactorObserver{fromBound: 8, done: make(chan struct{})}
    97  			compactor.Subscribe(&co)
    98  
    99  			// Run Compactor in background.
   100  			<-compactor.Ready()
   101  
   102  			log.Info().Msgf("compactor is ready")
   103  
   104  			rootState := l.InitialState()
   105  
   106  			// Generate the tree and create WAL
   107  			// update the ledger size (10) times, since each update will trigger a segment file creation
   108  			// and checkpointDistance is 3, then, 10 segment files should trigger generating checkpoint:
   109  			// 2, 5, 8, that's why the fromBound is 8
   110  			for i := 0; i < size; i++ {
   111  				// slow down updating the ledger, because running too fast would cause the previous checkpoint
   112  				// to not finish and get delayed
   113  				time.Sleep(LedgerUpdateDelay)
   114  
   115  				payloads := testutils.RandomPayloads(numInsPerStep, minPayloadByteSize, maxPayloadByteSize)
   116  
   117  				keys := make([]ledger.Key, len(payloads))
   118  				values := make([]ledger.Value, len(payloads))
   119  				for i, p := range payloads {
   120  					k, err := p.Key()
   121  					require.NoError(t, err)
   122  					keys[i] = k
   123  					values[i] = p.Value()
   124  				}
   125  
   126  				update, err := ledger.NewUpdate(rootState, keys, values)
   127  				require.NoError(t, err)
   128  
   129  				log.Info().Msgf("update ledger with min payload byte size: %v, %v will trigger segment file creation",
   130  					numInsPerStep*minPayloadByteSize, segmentSize)
   131  				newState, _, err := l.Set(update)
   132  				require.NoError(t, err)
   133  
   134  				require.FileExists(t, path.Join(dir, realWAL.NumberToFilenamePart(i)))
   135  
   136  				data := make(map[string]*ledger.Payload, len(keys))
   137  				for j, k := range keys {
   138  					ks := string(k.CanonicalForm())
   139  					data[ks] = payloads[j]
   140  				}
   141  
   142  				savedData[ledger.RootHash(newState)] = data
   143  
   144  				rootState = newState
   145  			}
   146  
   147  			log.Info().Msgf("wait for the bound-checking observer to confirm checkpoints have been made")
   148  
   149  			select {
   150  			case <-co.done:
   151  				// continue
   152  			case <-time.After(60 * time.Second):
   153  				// Log segment and checkpoint files
   154  				files, err := os.ReadDir(dir)
   155  				require.NoError(t, err)
   156  
   157  				for _, file := range files {
   158  					info, err := file.Info()
   159  					require.NoError(t, err)
   160  					fmt.Printf("%s, size %d\n", file.Name(), info.Size())
   161  				}
   162  
   163  				assert.FailNow(t, "timed out")
   164  			}
   165  
   166  			checkpointer, err := wal.NewCheckpointer()
   167  			require.NoError(t, err)
   168  
   169  			from, to, err := checkpointer.NotCheckpointedSegments()
   170  			require.NoError(t, err)
   171  
   172  			assert.True(t, from == 9 && to == 10, "from: %v, to: %v", from, to) // Make sure there is no leftover
   173  
   174  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000000"))
   175  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000001"))
   176  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000002"))
   177  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000003"))
   178  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000004"))
   179  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000005"))
   180  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000006"))
   181  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000007"))
   182  			require.FileExists(t, path.Join(dir, "checkpoint.00000008"))
   183  			require.NoFileExists(t, path.Join(dir, "checkpoint.00000009"))
   184  
   185  			<-l.Done()
   186  			<-compactor.Done()
   187  		})
   188  
   189  		time.Sleep(2 * time.Second)
   190  
   191  		t.Run("remove unnecessary files", func(t *testing.T) {
   192  			// Remove all files apart from target checkpoint and WAL segments ahead of it
   193  			// We know their names, so just hardcode them
   194  			dirF, _ := os.Open(dir)
   195  			files, _ := dirF.Readdir(0)
   196  
   197  			find008, err := regexp.Compile("checkpoint.00000008*")
   198  			require.NoError(t, err)
   199  
   200  			for _, fileInfo := range files {
   201  
   202  				name := fileInfo.Name()
   203  				if name == "00000009" ||
   204  					name == "00000010" ||
   205  					name == "00000011" {
   206  					log.Info().Msgf("keep file %v/%v", dir, name)
   207  					continue
   208  				}
   209  
   210  				// checkpoint V6 has multiple files
   211  				if find008.MatchString(name) {
   212  					log.Info().Msgf("keep file %v/%v", dir, name)
   213  					continue
   214  				}
   215  
   216  				err := os.Remove(path.Join(dir, name))
   217  				require.NoError(t, err)
   218  				log.Info().Msgf("removed file %v/%v", dir, name)
   219  			}
   220  		})
   221  
   222  		var l2 *Ledger
   223  
   224  		time.Sleep(2 * time.Second)
   225  
   226  		t.Run("load data from checkpoint and WAL", func(t *testing.T) {
   227  
   228  			wal2, err := realWAL.NewDiskWAL(unittest.Logger(), nil, metrics.NewNoopCollector(), dir, size*10, pathByteSize, 32*1024)
   229  			require.NoError(t, err)
   230  
   231  			l2, err = NewLedger(wal2, size*10, metricsCollector, unittest.Logger(), DefaultPathFinderVersion)
   232  			require.NoError(t, err)
   233  
   234  			<-wal2.Done()
   235  		})
   236  
   237  		t.Run("make sure forests are equal", func(t *testing.T) {
   238  
   239  			// Check for same data
   240  			for rootHash, data := range savedData {
   241  
   242  				keys := make([]ledger.Key, 0, len(data))
   243  				for _, p := range data {
   244  					k, err := p.Key()
   245  					require.NoError(t, err)
   246  					keys = append(keys, k)
   247  				}
   248  
   249  				q, err := ledger.NewQuery(ledger.State(rootHash), keys)
   250  				require.NoError(t, err)
   251  
   252  				values, err := l.Get(q)
   253  				require.NoError(t, err)
   254  
   255  				values2, err := l2.Get(q)
   256  				require.NoError(t, err)
   257  
   258  				for i, k := range keys {
   259  					ks := k.CanonicalForm()
   260  					require.Equal(t, data[string(ks)].Value(), values[i])
   261  					require.Equal(t, data[string(ks)].Value(), values2[i])
   262  				}
   263  			}
   264  
   265  			forestTries, err := l.Tries()
   266  			require.NoError(t, err)
   267  
   268  			forestTriesSet := make(map[ledger.RootHash]struct{})
   269  			for _, trie := range forestTries {
   270  				forestTriesSet[trie.RootHash()] = struct{}{}
   271  			}
   272  
   273  			forestTries2, err := l.Tries()
   274  			require.NoError(t, err)
   275  
   276  			forestTries2Set := make(map[ledger.RootHash]struct{})
   277  			for _, trie := range forestTries2 {
   278  				forestTries2Set[trie.RootHash()] = struct{}{}
   279  			}
   280  
   281  			require.Equal(t, forestTriesSet, forestTries2Set)
   282  		})
   283  
   284  	})
   285  }
   286  
   287  // TestCompactorSkipCheckpointing tests that only one
   288  // checkpointing is running at a time.
   289  func TestCompactorSkipCheckpointing(t *testing.T) {
   290  	const (
   291  		numInsPerStep      = 2
   292  		pathByteSize       = 32
   293  		minPayloadByteSize = 2 << 15
   294  		maxPayloadByteSize = 2 << 16
   295  		size               = 10
   296  		checkpointDistance = 1 // checkpointDistance 1 triggers checkpointing for every segment.
   297  		checkpointsToKeep  = 0
   298  		forestCapacity     = size * 10
   299  	)
   300  
   301  	metricsCollector := &metrics.NoopCollector{}
   302  
   303  	unittest.RunWithTempDir(t, func(dir string) {
   304  
   305  		var l *Ledger
   306  
   307  		// saved data after updates
   308  		savedData := make(map[ledger.RootHash]map[string]*ledger.Payload)
   309  
   310  		wal, err := realWAL.NewDiskWAL(unittest.Logger(), nil, metrics.NewNoopCollector(), dir, forestCapacity, pathByteSize, 32*1024)
   311  		require.NoError(t, err)
   312  
   313  		l, err = NewLedger(wal, size*10, metricsCollector, zerolog.Logger{}, DefaultPathFinderVersion)
   314  		require.NoError(t, err)
   315  
   316  		// WAL segments are 32kB, so here we generate 2 keys 64kB each, times `size`
   317  		// so we should get at least `size` segments
   318  
   319  		compactor, err := NewCompactor(l, wal, unittest.Logger(), forestCapacity, checkpointDistance, checkpointsToKeep, atomic.NewBool(false))
   320  		require.NoError(t, err)
   321  
   322  		co := CompactorObserver{fromBound: 8, done: make(chan struct{})}
   323  		compactor.Subscribe(&co)
   324  
   325  		// Run Compactor in background.
   326  		<-compactor.Ready()
   327  
   328  		rootState := l.InitialState()
   329  
   330  		// Generate the tree and create WAL
   331  		for i := 0; i < size; i++ {
   332  
   333  			// slow down updating the ledger, because running too fast would cause the previous checkpoint
   334  			// to not finish and get delayed
   335  			time.Sleep(LedgerUpdateDelay)
   336  
   337  			payloads := testutils.RandomPayloads(numInsPerStep, minPayloadByteSize, maxPayloadByteSize)
   338  
   339  			keys := make([]ledger.Key, len(payloads))
   340  			values := make([]ledger.Value, len(payloads))
   341  			for i, p := range payloads {
   342  				k, err := p.Key()
   343  				require.NoError(t, err)
   344  				keys[i] = k
   345  				values[i] = p.Value()
   346  			}
   347  
   348  			update, err := ledger.NewUpdate(rootState, keys, values)
   349  			require.NoError(t, err)
   350  
   351  			newState, _, err := l.Set(update)
   352  			require.NoError(t, err)
   353  
   354  			require.FileExists(t, path.Join(dir, realWAL.NumberToFilenamePart(i)))
   355  
   356  			data := make(map[string]*ledger.Payload, len(keys))
   357  			for j, k := range keys {
   358  				ks := string(k.CanonicalForm())
   359  				data[ks] = payloads[j]
   360  			}
   361  
   362  			savedData[ledger.RootHash(newState)] = data
   363  
   364  			rootState = newState
   365  		}
   366  
   367  		// wait for the bound-checking observer to confirm checkpoints have been made
   368  		select {
   369  		case <-co.done:
   370  			// continue
   371  		case <-time.After(60 * time.Second):
   372  			// Log segment and checkpoint files
   373  			files, err := os.ReadDir(dir)
   374  			require.NoError(t, err)
   375  
   376  			for _, file := range files {
   377  				info, err := file.Info()
   378  				require.NoError(t, err)
   379  				fmt.Printf("%s, size %d\n", file.Name(), info.Size())
   380  			}
   381  
   382  			// This assert can be flaky because of speed fluctuations (GitHub CI slowdowns, etc.).
   383  			// Because this test only cares about number of created checkpoint files,
   384  			// we don't need to fail the test here and keeping commented out for documentation.
   385  			// assert.FailNow(t, "timed out")
   386  		}
   387  
   388  		<-l.Done()
   389  		<-compactor.Done()
   390  
   391  		first, last, err := wal.Segments()
   392  		require.NoError(t, err)
   393  
   394  		segmentCount := last - first + 1
   395  
   396  		checkpointer, err := wal.NewCheckpointer()
   397  		require.NoError(t, err)
   398  
   399  		nums, err := checkpointer.Checkpoints()
   400  		require.NoError(t, err)
   401  
   402  		// Check that there are gaps between checkpoints (some checkpoints are skipped)
   403  		firstNum, lastNum := nums[0], nums[len(nums)-1]
   404  		require.True(t, (len(nums) < lastNum-firstNum+1) || (len(nums) < segmentCount))
   405  	})
   406  }
   407  
   408  // TestCompactorAccuracy expects checkpointed tries to match replayed tries.
   409  // Replayed tries are tries updated by replaying all WAL segments
   410  // (from segment 0, ignoring prior checkpoints) to the checkpoint number.
   411  // This verifies that checkpointed tries are snapshopt of segments and at segment boundary.
   412  func TestCompactorAccuracy(t *testing.T) {
   413  
   414  	const (
   415  		numInsPerStep      = 2
   416  		pathByteSize       = 32
   417  		minPayloadByteSize = 2<<11 - 256 // 3840 bytes
   418  		maxPayloadByteSize = 2 << 11     // 4096 bytes
   419  		size               = 20
   420  		checkpointDistance = 5
   421  		checkpointsToKeep  = 0 // keep all
   422  		forestCapacity     = 500
   423  	)
   424  
   425  	metricsCollector := &metrics.NoopCollector{}
   426  
   427  	unittest.RunWithTempDir(t, func(dir string) {
   428  
   429  		// There appears to be 1-2 records per segment (according to logs), so
   430  		// generate size/2 segments.
   431  
   432  		lastCheckpointNum := -1
   433  
   434  		rootHash := trie.EmptyTrieRootHash()
   435  
   436  		// Create DiskWAL and Ledger repeatedly to test rebuilding ledger state at restart.
   437  		for i := 0; i < 3; i++ {
   438  
   439  			wal, err := realWAL.NewDiskWAL(unittest.Logger(), nil, metrics.NewNoopCollector(), dir, forestCapacity, pathByteSize, 32*1024)
   440  			require.NoError(t, err)
   441  
   442  			l, err := NewLedger(wal, forestCapacity, metricsCollector, zerolog.Logger{}, DefaultPathFinderVersion)
   443  			require.NoError(t, err)
   444  
   445  			compactor, err := NewCompactor(l, wal, unittest.Logger(), forestCapacity, checkpointDistance, checkpointsToKeep, atomic.NewBool(false))
   446  			require.NoError(t, err)
   447  
   448  			fromBound := lastCheckpointNum + (size / 2)
   449  
   450  			co := CompactorObserver{fromBound: fromBound, done: make(chan struct{})}
   451  			compactor.Subscribe(&co)
   452  
   453  			// Run Compactor in background.
   454  			<-compactor.Ready()
   455  
   456  			// Generate the tree and create WAL
   457  			// size+2 is used to ensure that size/2 segments are finalized.
   458  			for i := 0; i < size+2; i++ {
   459  				// slow down updating the ledger, because running too fast would cause the previous checkpoint
   460  				// to not finish and get delayed
   461  				time.Sleep(LedgerUpdateDelay)
   462  
   463  				payloads := testutils.RandomPayloads(numInsPerStep, minPayloadByteSize, maxPayloadByteSize)
   464  
   465  				keys := make([]ledger.Key, len(payloads))
   466  				values := make([]ledger.Value, len(payloads))
   467  				for i, p := range payloads {
   468  					k, err := p.Key()
   469  					require.NoError(t, err)
   470  					keys[i] = k
   471  					values[i] = p.Value()
   472  				}
   473  
   474  				update, err := ledger.NewUpdate(ledger.State(rootHash), keys, values)
   475  				require.NoError(t, err)
   476  
   477  				newState, _, err := l.Set(update)
   478  				require.NoError(t, err)
   479  
   480  				rootHash = ledger.RootHash(newState)
   481  			}
   482  
   483  			// wait for the bound-checking observer to confirm checkpoints have been made
   484  			select {
   485  			case <-co.done:
   486  				// continue
   487  			case <-time.After(60 * time.Second):
   488  				assert.FailNow(t, "timed out")
   489  			}
   490  
   491  			// Shutdown ledger and compactor
   492  			<-l.Done()
   493  			<-compactor.Done()
   494  
   495  			checkpointer, err := wal.NewCheckpointer()
   496  			require.NoError(t, err)
   497  
   498  			nums, err := checkpointer.Checkpoints()
   499  			require.NoError(t, err)
   500  
   501  			for _, n := range nums {
   502  				// TODO:  After the LRU Cache (outside of checkpointing code) is replaced
   503  				// by a queue, etc. we should make sure the insertion order is preserved.
   504  
   505  				checkSequence := false
   506  				if i == 0 {
   507  					// Sequence check only works when initial state is blank.
   508  					// When initial state is from ledger's forest (LRU cache),
   509  					// its sequence is altered by reads when replaying segment records.
   510  					// Insertion order is not preserved (which is the way
   511  					// it is currently on mainnet).  However, with PR 2792, only the
   512  					// initial values are affected and those would likely
   513  					// get into insertion order for the next checkpoint.  Once
   514  					// the LRU Cache (outside of checkpointing code) is replaced,
   515  					// then we can verify insertion order.
   516  					checkSequence = true
   517  				}
   518  				testCheckpointedTriesMatchReplayedTriesFromSegments(t, checkpointer, n, dir, checkSequence)
   519  			}
   520  
   521  			lastCheckpointNum = nums[len(nums)-1]
   522  		}
   523  	})
   524  }
   525  
   526  // TestCompactorTriggeredByAdminTool tests that the compactor will listen to the signal from admin tool
   527  // to trigger checkpoint when current segment file is finished.
   528  func TestCompactorTriggeredByAdminTool(t *testing.T) {
   529  
   530  	const (
   531  		numInsPerStep      = 2 // the number of payloads in each trie update
   532  		pathByteSize       = 32
   533  		minPayloadByteSize = 2<<11 - 256 // 3840 bytes
   534  		maxPayloadByteSize = 2 << 11     // 4096 bytes
   535  		size               = 20
   536  		checkpointDistance = 5   // create checkpoint on every 5 segment files
   537  		checkpointsToKeep  = 0   // keep all
   538  		forestCapacity     = 500 // the number of tries to be included in a checkpoint file
   539  	)
   540  
   541  	metricsCollector := &metrics.NoopCollector{}
   542  
   543  	unittest.RunWithTempDir(t, func(dir string) {
   544  
   545  		rootHash := trie.EmptyTrieRootHash()
   546  
   547  		// Create DiskWAL and Ledger repeatedly to test rebuilding ledger state at restart.
   548  
   549  		wal, err := realWAL.NewDiskWAL(unittest.LoggerWithName("wal"), nil, metrics.NewNoopCollector(), dir, forestCapacity, pathByteSize, 32*1024)
   550  		require.NoError(t, err)
   551  
   552  		l, err := NewLedger(wal, forestCapacity, metricsCollector, unittest.LoggerWithName("ledger"), DefaultPathFinderVersion)
   553  		require.NoError(t, err)
   554  
   555  		compactor, err := NewCompactor(l, wal, unittest.LoggerWithName("compactor"), forestCapacity, checkpointDistance, checkpointsToKeep, atomic.NewBool(true))
   556  		require.NoError(t, err)
   557  
   558  		fmt.Println("should stop as soon as segment 5 is generated, which should trigger checkpoint 5 to be created")
   559  		fmt.Println("note checkpoint 0 will be be notified to the CompactorObserver because checkpoint 0 is the root checkpoint")
   560  		fmt.Println("Why fromBound =5? because without forcing checkpoint to trigger, it won't trigger until segment 4 is finished, with ")
   561  		fmt.Println("forcing checkpoint to trigger, it will trigger when segment 0 and 5 is finished")
   562  		fromBound := 5
   563  
   564  		co := CompactorObserver{fromBound: fromBound, done: make(chan struct{})}
   565  		compactor.Subscribe(&co)
   566  
   567  		// Run Compactor in background.
   568  		<-compactor.Ready()
   569  
   570  		fmt.Println("generate the tree and create WAL")
   571  		fmt.Println("2 trie updates will fill a segment file, and 12 trie updates will fill 6 segment files")
   572  		fmt.Println("13 trie updates in total will trigger segment 5 to be finished, which should trigger checkpoint 5")
   573  		for i := 0; i < 13; i++ {
   574  			// slow down updating the ledger, because running too fast would cause the previous checkpoint
   575  			// to not finish and get delayed
   576  			time.Sleep(LedgerUpdateDelay)
   577  
   578  			payloads := testutils.RandomPayloads(numInsPerStep, minPayloadByteSize, maxPayloadByteSize)
   579  
   580  			keys := make([]ledger.Key, len(payloads))
   581  			values := make([]ledger.Value, len(payloads))
   582  			for i, p := range payloads {
   583  				k, err := p.Key()
   584  				require.NoError(t, err)
   585  				keys[i] = k
   586  				values[i] = p.Value()
   587  			}
   588  
   589  			update, err := ledger.NewUpdate(ledger.State(rootHash), keys, values)
   590  			require.NoError(t, err)
   591  
   592  			newState, _, err := l.Set(update)
   593  			require.NoError(t, err)
   594  
   595  			rootHash = ledger.RootHash(newState)
   596  		}
   597  
   598  		// wait for the bound-checking observer to confirm checkpoints have been made
   599  		select {
   600  		case <-co.done:
   601  			// continue
   602  		case <-time.After(60 * time.Second):
   603  			assert.FailNow(t, "timed out")
   604  		}
   605  
   606  		// Shutdown ledger and compactor
   607  		<-l.Done()
   608  		<-compactor.Done()
   609  
   610  		checkpointer, err := wal.NewCheckpointer()
   611  		require.NoError(t, err)
   612  
   613  		nums, err := checkpointer.Checkpoints()
   614  		require.NoError(t, err)
   615  		// 0 is the first checkpoint triggered because of the force triggering
   616  		// 5 is triggered after 4 segments are filled.
   617  		require.Equal(t, []int{0, 5}, nums)
   618  	})
   619  }
   620  
   621  // TestCompactorConcurrency expects checkpointed tries to
   622  // match replayed tries in sequence with concurrent updates.
   623  // Replayed tries are tries updated by replaying all WAL segments
   624  // (from segment 0, ignoring prior checkpoints) to the checkpoint number.
   625  // This verifies that checkpointed tries are snapshopt of segments
   626  // and at segment boundary.
   627  // Note: sequence check only works when initial state is blank.
   628  // When initial state is from ledger's forest (LRU cache), its
   629  // sequence is altered by reads when replaying segment records.
   630  func TestCompactorConcurrency(t *testing.T) {
   631  	const (
   632  		numInsPerStep      = 2
   633  		pathByteSize       = 32
   634  		minPayloadByteSize = 2<<11 - 256 // 3840 bytes
   635  		maxPayloadByteSize = 2 << 11     // 4096 bytes
   636  		size               = 20
   637  		checkpointDistance = 5
   638  		checkpointsToKeep  = 0 // keep all
   639  		forestCapacity     = 500
   640  		numGoroutine       = 4
   641  		lastCheckpointNum  = -1
   642  	)
   643  
   644  	rootState := ledger.State(trie.EmptyTrieRootHash())
   645  
   646  	metricsCollector := &metrics.NoopCollector{}
   647  
   648  	unittest.RunWithTempDir(t, func(dir string) {
   649  
   650  		// There are 1-2 records per segment (according to logs), so
   651  		// generate size/2 segments.
   652  
   653  		wal, err := realWAL.NewDiskWAL(unittest.Logger(), nil, metrics.NewNoopCollector(), dir, forestCapacity, pathByteSize, 32*1024)
   654  		require.NoError(t, err)
   655  
   656  		l, err := NewLedger(wal, forestCapacity, metricsCollector, zerolog.Logger{}, DefaultPathFinderVersion)
   657  		require.NoError(t, err)
   658  
   659  		compactor, err := NewCompactor(l, wal, unittest.Logger(), forestCapacity, checkpointDistance, checkpointsToKeep, atomic.NewBool(false))
   660  		require.NoError(t, err)
   661  
   662  		fromBound := lastCheckpointNum + (size / 2 * numGoroutine)
   663  
   664  		co := CompactorObserver{fromBound: fromBound, done: make(chan struct{})}
   665  		compactor.Subscribe(&co)
   666  
   667  		// Run Compactor in background.
   668  		<-compactor.Ready()
   669  
   670  		var wg sync.WaitGroup
   671  		wg.Add(numGoroutine)
   672  
   673  		// Run 4 goroutines and each goroutine updates size+1 tries.
   674  		for j := 0; j < numGoroutine; j++ {
   675  			go func(parentState ledger.State) {
   676  				defer wg.Done()
   677  
   678  				// size+1 is used to ensure that size/2*numGoroutine segments are finalized.
   679  				for i := 0; i < size+1; i++ {
   680  					// slow down updating the ledger, because running too fast would cause the previous checkpoint
   681  					// to not finish and get delayed
   682  					time.Sleep(LedgerUpdateDelay)
   683  					payloads := testutils.RandomPayloads(numInsPerStep, minPayloadByteSize, maxPayloadByteSize)
   684  
   685  					keys := make([]ledger.Key, len(payloads))
   686  					values := make([]ledger.Value, len(payloads))
   687  					for i, p := range payloads {
   688  						k, err := p.Key()
   689  						require.NoError(t, err)
   690  						keys[i] = k
   691  						values[i] = p.Value()
   692  					}
   693  
   694  					update, err := ledger.NewUpdate(parentState, keys, values)
   695  					require.NoError(t, err)
   696  
   697  					newState, _, err := l.Set(update)
   698  					require.NoError(t, err)
   699  
   700  					parentState = newState
   701  				}
   702  			}(rootState)
   703  		}
   704  
   705  		// wait for goroutines updating ledger
   706  		wg.Wait()
   707  
   708  		// wait for the bound-checking observer to confirm checkpoints have been made
   709  		select {
   710  		case <-co.done:
   711  			// continue
   712  		case <-time.After(120 * time.Second):
   713  			assert.FailNow(t, "timed out")
   714  		}
   715  
   716  		// Shutdown ledger and compactor
   717  		<-l.Done()
   718  		<-compactor.Done()
   719  
   720  		checkpointer, err := wal.NewCheckpointer()
   721  		require.NoError(t, err)
   722  
   723  		nums, err := checkpointer.Checkpoints()
   724  		require.NoError(t, err)
   725  
   726  		for _, n := range nums {
   727  			// For each created checkpoint:
   728  			// - get tries by loading checkpoint
   729  			// - get tries by replaying segments up to checkpoint number (ignoring all prior checkpoints)
   730  			// - test that these 2 sets of tries match in content and sequence (insertion order).
   731  			testCheckpointedTriesMatchReplayedTriesFromSegments(t, checkpointer, n, dir, true)
   732  		}
   733  	})
   734  }
   735  
   736  func testCheckpointedTriesMatchReplayedTriesFromSegments(
   737  	t *testing.T,
   738  	checkpointer *realWAL.Checkpointer,
   739  	checkpointNum int,
   740  	dir string,
   741  	inSequence bool,
   742  ) {
   743  	// Get tries by loading checkpoint
   744  	triesFromLoadingCheckpoint, err := checkpointer.LoadCheckpoint(checkpointNum)
   745  	require.NoError(t, err)
   746  
   747  	// Get tries by replaying segments up to checkpoint number (ignoring checkpoints)
   748  	triesFromReplayingSegments, err := triesUpToSegment(dir, checkpointNum, len(triesFromLoadingCheckpoint))
   749  	require.NoError(t, err)
   750  
   751  	if inSequence {
   752  		// Test that checkpointed tries match replayed tries in content and sequence (insertion order).
   753  		require.Equal(t, len(triesFromReplayingSegments), len(triesFromLoadingCheckpoint))
   754  		for i := 0; i < len(triesFromReplayingSegments); i++ {
   755  			require.Equal(t, triesFromReplayingSegments[i].RootHash(), triesFromLoadingCheckpoint[i].RootHash())
   756  		}
   757  		return
   758  	}
   759  
   760  	// Test that checkpointed tries match replayed tries in content (ignore order).
   761  	triesSetFromReplayingSegments := make(map[ledger.RootHash]struct{})
   762  	for _, t := range triesFromReplayingSegments {
   763  		triesSetFromReplayingSegments[t.RootHash()] = struct{}{}
   764  	}
   765  
   766  	triesSetFromLoadingCheckpoint := make(map[ledger.RootHash]struct{})
   767  	for _, t := range triesFromLoadingCheckpoint {
   768  		triesSetFromLoadingCheckpoint[t.RootHash()] = struct{}{}
   769  	}
   770  
   771  	require.True(t, reflect.DeepEqual(triesSetFromReplayingSegments, triesSetFromLoadingCheckpoint))
   772  }
   773  
   774  func triesUpToSegment(dir string, to int, capacity int) ([]*trie.MTrie, error) {
   775  
   776  	// forest is used to create new trie.
   777  	forest, err := mtrie.NewForest(capacity, &metrics.NoopCollector{}, nil)
   778  	if err != nil {
   779  		return nil, fmt.Errorf("cannot create Forest: %w", err)
   780  	}
   781  
   782  	initialTries, err := forest.GetTries()
   783  	if err != nil {
   784  		return nil, fmt.Errorf("cannot get tries from forest: %w", err)
   785  	}
   786  
   787  	// TrieQueue is used to store last n tries from segment files in order (n = capacity)
   788  	tries := realWAL.NewTrieQueueWithValues(uint(capacity), initialTries)
   789  
   790  	err = replaySegments(
   791  		dir,
   792  		to,
   793  		func(update *ledger.TrieUpdate) error {
   794  			t, err := forest.NewTrie(update)
   795  			if err == nil {
   796  				err = forest.AddTrie(t)
   797  				if err != nil {
   798  					return err
   799  				}
   800  				tries.Push(t)
   801  			}
   802  			return err
   803  		}, func(rootHash ledger.RootHash) error {
   804  			return nil
   805  		})
   806  	if err != nil {
   807  		return nil, err
   808  	}
   809  
   810  	return tries.Tries(), nil
   811  }
   812  
   813  func replaySegments(
   814  	dir string,
   815  	to int,
   816  	updateFn func(update *ledger.TrieUpdate) error,
   817  	deleteFn func(rootHash ledger.RootHash) error,
   818  ) error {
   819  	sr, err := prometheusWAL.NewSegmentsRangeReader(prometheusWAL.SegmentRange{
   820  		Dir:   dir,
   821  		First: 0,
   822  		Last:  to,
   823  	})
   824  	if err != nil {
   825  		return fmt.Errorf("cannot create segment reader: %w", err)
   826  	}
   827  
   828  	reader := prometheusWAL.NewReader(sr)
   829  
   830  	defer sr.Close()
   831  
   832  	for reader.Next() {
   833  		record := reader.Record()
   834  		operation, rootHash, update, err := realWAL.Decode(record)
   835  		if err != nil {
   836  			return fmt.Errorf("cannot decode LedgerWAL record: %w", err)
   837  		}
   838  
   839  		switch operation {
   840  		case realWAL.WALUpdate:
   841  			err = updateFn(update)
   842  			if err != nil {
   843  				return fmt.Errorf("error while processing LedgerWAL update: %w", err)
   844  			}
   845  		case realWAL.WALDelete:
   846  			err = deleteFn(rootHash)
   847  			if err != nil {
   848  				return fmt.Errorf("error while processing LedgerWAL deletion: %w", err)
   849  			}
   850  		}
   851  
   852  		err = reader.Err()
   853  		if err != nil {
   854  			return fmt.Errorf("cannot read LedgerWAL: %w", err)
   855  		}
   856  	}
   857  
   858  	return nil
   859  }