github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/core/bloombits/matcher_test.go (about) 1 // This file is part of the go-sberex library. The go-sberex library is 2 // free software: you can redistribute it and/or modify it under the terms 3 // of the GNU Lesser General Public License as published by the Free 4 // Software Foundation, either version 3 of the License, or (at your option) 5 // any later version. 6 // 7 // The go-sberex library is distributed in the hope that it will be useful, 8 // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 10 // General Public License <http://www.gnu.org/licenses/> for more details. 11 12 package bloombits 13 14 import ( 15 "context" 16 "math/rand" 17 "sync/atomic" 18 "testing" 19 "time" 20 21 "github.com/Sberex/go-sberex/common" 22 ) 23 24 const testSectionSize = 4096 25 26 // Tests that wildcard filter rules (nil) can be specified and are handled well. 27 func TestMatcherWildcards(t *testing.T) { 28 matcher := NewMatcher(testSectionSize, [][][]byte{ 29 {common.Address{}.Bytes(), common.Address{0x01}.Bytes()}, // Default address is not a wildcard 30 {common.Hash{}.Bytes(), common.Hash{0x01}.Bytes()}, // Default hash is not a wildcard 31 {common.Hash{0x01}.Bytes()}, // Plain rule, sanity check 32 {common.Hash{0x01}.Bytes(), nil}, // Wildcard suffix, drop rule 33 {nil, common.Hash{0x01}.Bytes()}, // Wildcard prefix, drop rule 34 {nil, nil}, // Wildcard combo, drop rule 35 {}, // Inited wildcard rule, drop rule 36 nil, // Proper wildcard rule, drop rule 37 }) 38 if len(matcher.filters) != 3 { 39 t.Fatalf("filter system size mismatch: have %d, want %d", len(matcher.filters), 3) 40 } 41 if len(matcher.filters[0]) != 2 { 42 t.Fatalf("address clause size mismatch: have %d, want %d", len(matcher.filters[0]), 2) 43 } 44 if len(matcher.filters[1]) != 2 { 45 t.Fatalf("combo topic clause size mismatch: have %d, want %d", len(matcher.filters[1]), 2) 46 } 47 if len(matcher.filters[2]) != 1 { 48 t.Fatalf("singletone topic clause size mismatch: have %d, want %d", len(matcher.filters[2]), 1) 49 } 50 } 51 52 // Tests the matcher pipeline on a single continuous workflow without interrupts. 53 func TestMatcherContinuous(t *testing.T) { 54 testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, false, 75) 55 testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, false, 81) 56 testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, false, 36) 57 } 58 59 // Tests the matcher pipeline on a constantly interrupted and resumed work pattern 60 // with the aim of ensuring data items are requested only once. 61 func TestMatcherIntermittent(t *testing.T) { 62 testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, true, 75) 63 testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, true, 81) 64 testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, true, 36) 65 } 66 67 // Tests the matcher pipeline on random input to hopefully catch anomalies. 68 func TestMatcherRandom(t *testing.T) { 69 for i := 0; i < 10; i++ { 70 testMatcherBothModes(t, makeRandomIndexes([]int{1}, 50), 0, 10000, 0) 71 testMatcherBothModes(t, makeRandomIndexes([]int{3}, 50), 0, 10000, 0) 72 testMatcherBothModes(t, makeRandomIndexes([]int{2, 2, 2}, 20), 0, 10000, 0) 73 testMatcherBothModes(t, makeRandomIndexes([]int{5, 5, 5}, 50), 0, 10000, 0) 74 testMatcherBothModes(t, makeRandomIndexes([]int{4, 4, 4}, 20), 0, 10000, 0) 75 } 76 } 77 78 // Tests that the matcher can properly find matches if the starting block is 79 // shifter from a multiple of 8. This is needed to cover an optimisation with 80 // bitset matching. 81 func TestMatcherShifted(t *testing.T) { 82 // Block 0 always matches in the tests, skip ahead of first 8 blocks with the 83 // start to get a potential zero byte in the matcher bitset. 84 85 // To keep the second bitset byte zero, the filter must only match for the first 86 // time in block 16, so doing an all-16 bit filter should suffice. 87 88 // To keep the starting block non divisible by 8, block number 9 is the first 89 // that would introduce a shift and not match block 0. 90 testMatcherBothModes(t, [][]bloomIndexes{{{16, 16, 16}}}, 9, 64, 0) 91 } 92 93 // Tests that matching on everything doesn't crash (special case internally). 94 func TestWildcardMatcher(t *testing.T) { 95 testMatcherBothModes(t, nil, 0, 10000, 0) 96 } 97 98 // makeRandomIndexes generates a random filter system, composed on multiple filter 99 // criteria, each having one bloom list component for the address and arbitrarily 100 // many topic bloom list components. 101 func makeRandomIndexes(lengths []int, max int) [][]bloomIndexes { 102 res := make([][]bloomIndexes, len(lengths)) 103 for i, topics := range lengths { 104 res[i] = make([]bloomIndexes, topics) 105 for j := 0; j < topics; j++ { 106 for k := 0; k < len(res[i][j]); k++ { 107 res[i][j][k] = uint(rand.Intn(max-1) + 2) 108 } 109 } 110 } 111 return res 112 } 113 114 // testMatcherDiffBatches runs the given matches test in single-delivery and also 115 // in batches delivery mode, verifying that all kinds of deliveries are handled 116 // correctly withn. 117 func testMatcherDiffBatches(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32) { 118 singleton := testMatcher(t, filter, start, blocks, intermittent, retrievals, 1) 119 batched := testMatcher(t, filter, start, blocks, intermittent, retrievals, 16) 120 121 if singleton != batched { 122 t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, %v in signleton vs. %v in batched mode", filter, blocks, intermittent, singleton, batched) 123 } 124 } 125 126 // testMatcherBothModes runs the given matcher test in both continuous as well as 127 // in intermittent mode, verifying that the request counts match each other. 128 func testMatcherBothModes(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, retrievals uint32) { 129 continuous := testMatcher(t, filter, start, blocks, false, retrievals, 16) 130 intermittent := testMatcher(t, filter, start, blocks, true, retrievals, 16) 131 132 if continuous != intermittent { 133 t.Errorf("filter = %v blocks = %v: request count mismatch, %v in continuous vs. %v in intermittent mode", filter, blocks, continuous, intermittent) 134 } 135 } 136 137 // testMatcher is a generic tester to run the given matcher test and return the 138 // number of requests made for cross validation between different modes. 139 func testMatcher(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32, maxReqCount int) uint32 { 140 // Create a new matcher an simulate our explicit random bitsets 141 matcher := NewMatcher(testSectionSize, nil) 142 matcher.filters = filter 143 144 for _, rule := range filter { 145 for _, topic := range rule { 146 for _, bit := range topic { 147 matcher.addScheduler(bit) 148 } 149 } 150 } 151 // Track the number of retrieval requests made 152 var requested uint32 153 154 // Start the matching session for the filter and the retriver goroutines 155 quit := make(chan struct{}) 156 matches := make(chan uint64, 16) 157 158 session, err := matcher.Start(context.Background(), start, blocks-1, matches) 159 if err != nil { 160 t.Fatalf("failed to stat matcher session: %v", err) 161 } 162 startRetrievers(session, quit, &requested, maxReqCount) 163 164 // Iterate over all the blocks and verify that the pipeline produces the correct matches 165 for i := start; i < blocks; i++ { 166 if expMatch3(filter, i) { 167 match, ok := <-matches 168 if !ok { 169 t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, results channel closed", filter, blocks, intermittent, i) 170 return 0 171 } 172 if match != i { 173 t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, got #%v", filter, blocks, intermittent, i, match) 174 } 175 // If we're testing intermittent mode, abort and restart the pipeline 176 if intermittent { 177 session.Close() 178 close(quit) 179 180 quit = make(chan struct{}) 181 matches = make(chan uint64, 16) 182 183 session, err = matcher.Start(context.Background(), i+1, blocks-1, matches) 184 if err != nil { 185 t.Fatalf("failed to stat matcher session: %v", err) 186 } 187 startRetrievers(session, quit, &requested, maxReqCount) 188 } 189 } 190 } 191 // Ensure the result channel is torn down after the last block 192 match, ok := <-matches 193 if ok { 194 t.Errorf("filter = %v blocks = %v intermittent = %v: expected closed channel, got #%v", filter, blocks, intermittent, match) 195 } 196 // Clean up the session and ensure we match the expected retrieval count 197 session.Close() 198 close(quit) 199 200 if retrievals != 0 && requested != retrievals { 201 t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, have #%v, want #%v", filter, blocks, intermittent, requested, retrievals) 202 } 203 return requested 204 } 205 206 // startRetrievers starts a batch of goroutines listening for section requests 207 // and serving them. 208 func startRetrievers(session *MatcherSession, quit chan struct{}, retrievals *uint32, batch int) { 209 requests := make(chan chan *Retrieval) 210 211 for i := 0; i < 10; i++ { 212 // Start a multiplexer to test multiple threaded execution 213 go session.Multiplex(batch, 100*time.Microsecond, requests) 214 215 // Start a services to match the above multiplexer 216 go func() { 217 for { 218 // Wait for a service request or a shutdown 219 select { 220 case <-quit: 221 return 222 223 case request := <-requests: 224 task := <-request 225 226 task.Bitsets = make([][]byte, len(task.Sections)) 227 for i, section := range task.Sections { 228 if rand.Int()%4 != 0 { // Handle occasional missing deliveries 229 task.Bitsets[i] = generateBitset(task.Bit, section) 230 atomic.AddUint32(retrievals, 1) 231 } 232 } 233 request <- task 234 } 235 } 236 }() 237 } 238 } 239 240 // generateBitset generates the rotated bitset for the given bloom bit and section 241 // numbers. 242 func generateBitset(bit uint, section uint64) []byte { 243 bitset := make([]byte, testSectionSize/8) 244 for i := 0; i < len(bitset); i++ { 245 for b := 0; b < 8; b++ { 246 blockIdx := section*testSectionSize + uint64(i*8+b) 247 bitset[i] += bitset[i] 248 if (blockIdx % uint64(bit)) == 0 { 249 bitset[i]++ 250 } 251 } 252 } 253 return bitset 254 } 255 256 func expMatch1(filter bloomIndexes, i uint64) bool { 257 for _, ii := range filter { 258 if (i % uint64(ii)) != 0 { 259 return false 260 } 261 } 262 return true 263 } 264 265 func expMatch2(filter []bloomIndexes, i uint64) bool { 266 for _, ii := range filter { 267 if expMatch1(ii, i) { 268 return true 269 } 270 } 271 return false 272 } 273 274 func expMatch3(filter [][]bloomIndexes, i uint64) bool { 275 for _, ii := range filter { 276 if !expMatch2(ii, i) { 277 return false 278 } 279 } 280 return true 281 }