|
| 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_LAST_CONFIG] = protoutil.MarshalOrPanic(&common.Metadata{ |
| 120 | + Value: protoutil.MarshalOrPanic(&common.LastConfig{Index: 9}), |
| 121 | + }) |
| 122 | + |
| 123 | + puller := &mocks.ChainPuller{} |
| 124 | + puller.On("Close") |
| 125 | + puller.On("HeightsByEndpoints").Return(map[string]uint64{"foo": 10}, nil) |
| 126 | + puller.On("PullBlock", uint64(9)).Return(configBlock) |
| 127 | + |
| 128 | + for _, testCase := range []struct { |
| 129 | + description string |
| 130 | + expectedPanic string |
| 131 | + expectedLog string |
| 132 | + expectedCommittedBlockCount int |
| 133 | + amIInChannelReturns error |
| 134 | + evictionSuspicionThreshold time.Duration |
| 135 | + blockPuller BlockPuller |
| 136 | + blockPullerErr error |
| 137 | + height uint64 |
| 138 | + halt func() |
| 139 | + }{ |
| 140 | + { |
| 141 | + description: "suspected time is lower than threshold", |
| 142 | + evictionSuspicionThreshold: 11 * time.Minute, |
| 143 | + halt: t.Fail, |
| 144 | + }, |
| 145 | + { |
| 146 | + description: "puller creation fails", |
| 147 | + evictionSuspicionThreshold: 10*time.Minute - time.Second, |
| 148 | + blockPullerErr: errors.New("oops"), |
| 149 | + expectedPanic: "Failed creating a block puller: oops", |
| 150 | + halt: t.Fail, |
| 151 | + }, |
| 152 | + { |
| 153 | + description: "our height is the highest", |
| 154 | + expectedLog: "Our height is higher or equal than the height of the orderer we pulled the last block from, aborting", |
| 155 | + evictionSuspicionThreshold: 10*time.Minute - time.Second, |
| 156 | + blockPuller: puller, |
| 157 | + height: 10, |
| 158 | + halt: t.Fail, |
| 159 | + }, |
| 160 | + { |
| 161 | + description: "failed pulling the block", |
| 162 | + expectedLog: "Cannot confirm our own eviction from the channel: bad block", |
| 163 | + evictionSuspicionThreshold: 10*time.Minute - time.Second, |
| 164 | + amIInChannelReturns: errors.New("bad block"), |
| 165 | + blockPuller: puller, |
| 166 | + height: 9, |
| 167 | + halt: t.Fail, |
| 168 | + }, |
| 169 | + { |
| 170 | + description: "we are still in the channel", |
| 171 | + expectedLog: "Cannot confirm our own eviction from the channel, our certificate was found in config block with sequence 9", |
| 172 | + evictionSuspicionThreshold: 10*time.Minute - time.Second, |
| 173 | + amIInChannelReturns: nil, |
| 174 | + blockPuller: puller, |
| 175 | + height: 9, |
| 176 | + halt: t.Fail, |
| 177 | + }, |
| 178 | + { |
| 179 | + description: "we are not in the channel", |
| 180 | + expectedLog: "Detected our own eviction from the channel in block [9]", |
| 181 | + evictionSuspicionThreshold: 10*time.Minute - time.Second, |
| 182 | + amIInChannelReturns: cluster.ErrNotInChannel, |
| 183 | + blockPuller: puller, |
| 184 | + height: 8, |
| 185 | + expectedCommittedBlockCount: 2, |
| 186 | + halt: func() { |
| 187 | + puller.On("PullBlock", uint64(8)).Return(&common.Block{ |
| 188 | + Header: &common.BlockHeader{Number: 8}, |
| 189 | + Metadata: &common.BlockMetadata{ |
| 190 | + Metadata: [][]byte{{}, {}, {}, {}}, |
| 191 | + }, |
| 192 | + }) |
| 193 | + }, |
| 194 | + }, |
| 195 | + } { |
| 196 | + testCase := testCase |
| 197 | + t.Run(testCase.description, func(t *testing.T) { |
| 198 | + committedBlocks := make(chan *common.Block, 2) |
| 199 | + |
| 200 | + commitBlock := func(block *common.Block) error { |
| 201 | + committedBlocks <- block |
| 202 | + return nil |
| 203 | + } |
| 204 | + |
| 205 | + es := &evictionSuspector{ |
| 206 | + halt: testCase.halt, |
| 207 | + amIInChannel: func(_ *common.Block) error { |
| 208 | + return testCase.amIInChannelReturns |
| 209 | + }, |
| 210 | + evictionSuspicionThreshold: testCase.evictionSuspicionThreshold, |
| 211 | + createPuller: func() (BlockPuller, error) { |
| 212 | + return testCase.blockPuller, testCase.blockPullerErr |
| 213 | + }, |
| 214 | + writeBlock: commitBlock, |
| 215 | + height: func() uint64 { |
| 216 | + return testCase.height |
| 217 | + }, |
| 218 | + logger: flogging.MustGetLogger("test"), |
| 219 | + triggerCatchUp: func(sn *raftpb.Snapshot) { return }, |
| 220 | + } |
| 221 | + |
| 222 | + foundExpectedLog := testCase.expectedLog == "" |
| 223 | + es.logger = es.logger.WithOptions(zap.Hooks(func(entry zapcore.Entry) error { |
| 224 | + if strings.Contains(entry.Message, testCase.expectedLog) { |
| 225 | + foundExpectedLog = true |
| 226 | + } |
| 227 | + return nil |
| 228 | + })) |
| 229 | + |
| 230 | + runTestCase := func() { |
| 231 | + es.confirmSuspicion(time.Minute * 10) |
| 232 | + } |
| 233 | + |
| 234 | + if testCase.expectedPanic != "" { |
| 235 | + assert.PanicsWithValue(t, testCase.expectedPanic, runTestCase) |
| 236 | + } else { |
| 237 | + runTestCase() |
| 238 | + // Run the test case again. |
| 239 | + // Conditions that do not lead to a conclusion of a chain eviction |
| 240 | + // should be idempotent. |
| 241 | + // Conditions that do lead to conclusion of a chain eviction |
| 242 | + // in the second time - should result in a no-op. |
| 243 | + runTestCase() |
| 244 | + } |
| 245 | + |
| 246 | + assert.True(t, foundExpectedLog, "expected to find %s but didn't", testCase.expectedLog) |
| 247 | + assert.Equal(t, testCase.expectedCommittedBlockCount, len(committedBlocks)) |
| 248 | + }) |
| 249 | + } |
| 250 | +} |
0 commit comments