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 }