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