github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/orderer/consensus/etcdraft/eviction_test.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package etcdraft
     8  
     9  import (
    10  	"strings"
    11  	"sync/atomic"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hyperledger/fabric-protos-go/common"
    16  	"github.com/osdi23p228/fabric/common/flogging"
    17  	"github.com/osdi23p228/fabric/orderer/common/cluster"
    18  	"github.com/osdi23p228/fabric/orderer/common/cluster/mocks"
    19  	"github.com/osdi23p228/fabric/protoutil"
    20  	"github.com/onsi/gomega"
    21  	"github.com/pkg/errors"
    22  	"github.com/stretchr/testify/assert"
    23  	"go.etcd.io/etcd/raft/raftpb"
    24  	"go.uber.org/zap"
    25  	"go.uber.org/zap/zapcore"
    26  )
    27  
    28  func TestPeriodicCheck(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	g := gomega.NewGomegaWithT(t)
    32  
    33  	var cond uint32
    34  	var checkNum uint32
    35  
    36  	fiveChecks := func() bool {
    37  		return atomic.LoadUint32(&checkNum) > uint32(5)
    38  	}
    39  
    40  	condition := func() bool {
    41  		atomic.AddUint32(&checkNum, 1)
    42  		return atomic.LoadUint32(&cond) == uint32(1)
    43  	}
    44  
    45  	reports := make(chan time.Duration, 1000)
    46  
    47  	report := func(duration time.Duration) {
    48  		reports <- duration
    49  	}
    50  
    51  	clears := make(chan struct{}, 1000)
    52  
    53  	reportCleared := func() {
    54  		clears <- struct{}{}
    55  	}
    56  
    57  	check := &PeriodicCheck{
    58  		Logger:        flogging.MustGetLogger("test"),
    59  		Condition:     condition,
    60  		CheckInterval: time.Millisecond,
    61  		Report:        report,
    62  		ReportCleared: reportCleared,
    63  	}
    64  
    65  	go check.Run()
    66  
    67  	g.Eventually(fiveChecks, time.Minute, time.Millisecond).Should(gomega.BeTrue())
    68  	// trigger condition to be true
    69  	atomic.StoreUint32(&cond, 1)
    70  	g.Eventually(reports, time.Minute, time.Millisecond).Should(gomega.Not(gomega.BeEmpty()))
    71  	// read first report
    72  	firstReport := <-reports
    73  	g.Eventually(reports, time.Minute, time.Millisecond).Should(gomega.Not(gomega.BeEmpty()))
    74  	// read second report
    75  	secondReport := <-reports
    76  	// time increases between reports
    77  	g.Expect(secondReport).To(gomega.BeNumerically(">", firstReport))
    78  	// wait for the reports channel to be full
    79  	g.Eventually(func() int { return len(reports) }, time.Minute, time.Millisecond).Should(gomega.BeNumerically("==", 1000))
    80  
    81  	// trigger condition to be false
    82  	atomic.StoreUint32(&cond, 0)
    83  
    84  	var lastReport time.Duration
    85  	// drain the reports channel
    86  	for len(reports) > 0 {
    87  		select {
    88  		case report := <-reports:
    89  			lastReport = report
    90  		default:
    91  			break
    92  		}
    93  	}
    94  
    95  	g.Eventually(clears).Should(gomega.Receive())
    96  	g.Consistently(clears).ShouldNot(gomega.Receive())
    97  
    98  	// ensure the checks have been made
    99  	checksDoneSoFar := atomic.LoadUint32(&checkNum)
   100  	g.Consistently(reports, time.Second*2, time.Millisecond).Should(gomega.BeEmpty())
   101  	checksDoneAfter := atomic.LoadUint32(&checkNum)
   102  	g.Expect(checksDoneAfter).To(gomega.BeNumerically(">", checksDoneSoFar))
   103  	// but nothing has been reported
   104  	g.Expect(reports).To(gomega.BeEmpty())
   105  
   106  	// trigger the condition again
   107  	atomic.StoreUint32(&cond, 1)
   108  	g.Eventually(reports, time.Minute, time.Millisecond).Should(gomega.Not(gomega.BeEmpty()))
   109  	// The first report is smaller than the last report,
   110  	// so the countdown has been reset when the condition was reset
   111  	firstReport = <-reports
   112  	g.Expect(lastReport).To(gomega.BeNumerically(">", firstReport))
   113  	// Stop the periodic check.
   114  	check.Stop()
   115  	checkCountAfterStop := atomic.LoadUint32(&checkNum)
   116  	// Wait 50 times the check interval.
   117  	time.Sleep(check.CheckInterval * 50)
   118  	// Ensure that we cease checking the condition, hence the PeriodicCheck is stopped.
   119  	g.Expect(atomic.LoadUint32(&checkNum)).To(gomega.BeNumerically("<", checkCountAfterStop+2))
   120  	g.Consistently(clears).ShouldNot(gomega.Receive())
   121  }
   122  
   123  func TestEvictionSuspector(t *testing.T) {
   124  	configBlock := &common.Block{
   125  		Header: &common.BlockHeader{Number: 9},
   126  		Metadata: &common.BlockMetadata{
   127  			Metadata: [][]byte{{}, {}, {}, {}},
   128  		},
   129  	}
   130  	configBlock.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(&common.Metadata{
   131  		Value: protoutil.MarshalOrPanic(&common.OrdererBlockMetadata{
   132  			LastConfig: &common.LastConfig{Index: 9},
   133  		}),
   134  	})
   135  
   136  	puller := &mocks.ChainPuller{}
   137  	puller.On("Close")
   138  	puller.On("HeightsByEndpoints").Return(map[string]uint64{"foo": 10}, nil)
   139  	puller.On("PullBlock", uint64(9)).Return(configBlock)
   140  
   141  	for _, testCase := range []struct {
   142  		description                 string
   143  		expectedPanic               string
   144  		expectedLog                 string
   145  		expectedCommittedBlockCount int
   146  		amIInChannelReturns         error
   147  		evictionSuspicionThreshold  time.Duration
   148  		blockPuller                 BlockPuller
   149  		blockPullerErr              error
   150  		height                      uint64
   151  		timesTriggered              int
   152  		halt                        func()
   153  	}{
   154  		{
   155  			description:                "suspected time is lower than threshold",
   156  			evictionSuspicionThreshold: 11 * time.Minute,
   157  			halt:                       t.Fail,
   158  		},
   159  		{
   160  			description:                "timesTriggered multiplier prevents threshold",
   161  			evictionSuspicionThreshold: 6 * time.Minute,
   162  			timesTriggered:             1,
   163  			halt:                       t.Fail,
   164  		},
   165  		{
   166  			description:                "puller creation fails",
   167  			evictionSuspicionThreshold: 10*time.Minute - time.Second,
   168  			blockPullerErr:             errors.New("oops"),
   169  			expectedPanic:              "Failed creating a block puller: oops",
   170  			halt:                       t.Fail,
   171  		},
   172  		{
   173  			description:                "our height is the highest",
   174  			expectedLog:                "Our height is higher or equal than the height of the orderer we pulled the last block from, aborting",
   175  			evictionSuspicionThreshold: 10*time.Minute - time.Second,
   176  			blockPuller:                puller,
   177  			height:                     10,
   178  			halt:                       t.Fail,
   179  		},
   180  		{
   181  			description:                "failed pulling the block",
   182  			expectedLog:                "Cannot confirm our own eviction from the channel: bad block",
   183  			evictionSuspicionThreshold: 10*time.Minute - time.Second,
   184  			amIInChannelReturns:        errors.New("bad block"),
   185  			blockPuller:                puller,
   186  			height:                     9,
   187  			halt:                       t.Fail,
   188  		},
   189  		{
   190  			description:                "we are still in the channel",
   191  			expectedLog:                "Cannot confirm our own eviction from the channel, our certificate was found in config block with sequence 9",
   192  			evictionSuspicionThreshold: 10*time.Minute - time.Second,
   193  			amIInChannelReturns:        nil,
   194  			blockPuller:                puller,
   195  			height:                     9,
   196  			halt:                       t.Fail,
   197  		},
   198  		{
   199  			description:                 "we are not in the channel",
   200  			expectedLog:                 "Detected our own eviction from the channel in block [9]",
   201  			evictionSuspicionThreshold:  10*time.Minute - time.Second,
   202  			amIInChannelReturns:         cluster.ErrNotInChannel,
   203  			blockPuller:                 puller,
   204  			height:                      8,
   205  			expectedCommittedBlockCount: 2,
   206  			halt: func() {
   207  				puller.On("PullBlock", uint64(8)).Return(&common.Block{
   208  					Header: &common.BlockHeader{Number: 8},
   209  					Metadata: &common.BlockMetadata{
   210  						Metadata: [][]byte{{}, {}, {}, {}},
   211  					},
   212  				})
   213  			},
   214  		},
   215  	} {
   216  		testCase := testCase
   217  		t.Run(testCase.description, func(t *testing.T) {
   218  			committedBlocks := make(chan *common.Block, 2)
   219  
   220  			commitBlock := func(block *common.Block) error {
   221  				committedBlocks <- block
   222  				return nil
   223  			}
   224  
   225  			es := &evictionSuspector{
   226  				halt: testCase.halt,
   227  				amIInChannel: func(_ *common.Block) error {
   228  					return testCase.amIInChannelReturns
   229  				},
   230  				evictionSuspicionThreshold: testCase.evictionSuspicionThreshold,
   231  				createPuller: func() (BlockPuller, error) {
   232  					return testCase.blockPuller, testCase.blockPullerErr
   233  				},
   234  				writeBlock: commitBlock,
   235  				height: func() uint64 {
   236  					return testCase.height
   237  				},
   238  				logger:         flogging.MustGetLogger("test"),
   239  				triggerCatchUp: func(sn *raftpb.Snapshot) {},
   240  				timesTriggered: testCase.timesTriggered,
   241  			}
   242  
   243  			foundExpectedLog := testCase.expectedLog == ""
   244  			es.logger = es.logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error {
   245  				if strings.Contains(entry.Message, testCase.expectedLog) {
   246  					foundExpectedLog = true
   247  				}
   248  				return nil
   249  			}))
   250  
   251  			runTestCase := func() {
   252  				es.confirmSuspicion(time.Minute * 10)
   253  			}
   254  
   255  			if testCase.expectedPanic != "" {
   256  				assert.PanicsWithValue(t, testCase.expectedPanic, runTestCase)
   257  			} else {
   258  				runTestCase()
   259  				// Run the test case again.
   260  				// Conditions that do not lead to a conclusion of a chain eviction
   261  				// should be idempotent.
   262  				// Conditions that do lead to conclusion of a chain eviction
   263  				// in the second time - should result in a no-op.
   264  				runTestCase()
   265  			}
   266  
   267  			assert.True(t, foundExpectedLog, "expected to find %s but didn't", testCase.expectedLog)
   268  			assert.Equal(t, testCase.expectedCommittedBlockCount, len(committedBlocks))
   269  		})
   270  	}
   271  }