github.com/thiagoyeds/go-cloud@v0.26.0/pubsub/batcher/batcher_test.go (about) 1 // Copyright 2018 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package batcher_test 16 17 import ( 18 "bytes" 19 "context" 20 "errors" 21 "io" 22 "reflect" 23 "sync" 24 "sync/atomic" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "gocloud.dev/pubsub/batcher" 30 ) 31 32 func TestSplit(t *testing.T) { 33 tests := []struct { 34 n int 35 opts *batcher.Options 36 want []int 37 }{ 38 // Defaults. 39 {0, nil, nil}, 40 {1, nil, []int{1}}, 41 {10, nil, []int{10}}, 42 // MinBatchSize. 43 {4, &batcher.Options{MinBatchSize: 5}, nil}, 44 {8, &batcher.Options{MinBatchSize: 5, MaxBatchSize: 7}, []int{7}}, 45 // <= MaxBatchSize. 46 {5, &batcher.Options{MaxBatchSize: 5}, []int{5}}, 47 {9, &batcher.Options{MaxBatchSize: 10}, []int{9}}, 48 // > MaxBatchSize with MaxHandlers = 1. 49 {5, &batcher.Options{MaxBatchSize: 4}, []int{4}}, 50 {999, &batcher.Options{MaxBatchSize: 10}, []int{10}}, 51 // MaxBatchSize with MaxHandlers > 1. 52 {10, &batcher.Options{MaxBatchSize: 4, MaxHandlers: 2}, []int{4, 4}}, 53 {10, &batcher.Options{MaxBatchSize: 5, MaxHandlers: 2}, []int{5, 5}}, 54 {10, &batcher.Options{MaxBatchSize: 9, MaxHandlers: 2}, []int{9, 1}}, 55 {9, &batcher.Options{MaxBatchSize: 4, MaxHandlers: 3}, []int{4, 4, 1}}, 56 {10, &batcher.Options{MaxBatchSize: 4, MaxHandlers: 3}, []int{4, 4, 2}}, 57 // All 3 options together. 58 {8, &batcher.Options{MinBatchSize: 5, MaxBatchSize: 7, MaxHandlers: 2}, []int{7}}, 59 } 60 61 for _, test := range tests { 62 got := batcher.Split(test.n, test.opts) 63 if diff := cmp.Diff(got, test.want); diff != "" { 64 t.Errorf("%d/%#v: got %v want %v diff %s", test.n, test.opts, got, test.want, diff) 65 } 66 } 67 } 68 69 func TestSequential(t *testing.T) { 70 // Verify that sequential non-concurrent Adds to a batcher produce single-item batches. 71 // Since there is no concurrent work, the Batcher will always produce the items one at a time. 72 ctx := context.Background() 73 var got []int 74 e := errors.New("e") 75 b := batcher.New(reflect.TypeOf(int(0)), nil, func(items interface{}) error { 76 got = items.([]int) 77 return e 78 }) 79 for i := 0; i < 10; i++ { 80 err := b.Add(ctx, i) 81 if err != e { 82 t.Errorf("got %v, want %v", err, e) 83 } 84 want := []int{i} 85 if !cmp.Equal(got, want) { 86 t.Errorf("got %+v, want %+v", got, want) 87 } 88 } 89 } 90 91 type sizableItem struct { 92 byteSize int 93 } 94 95 func (i *sizableItem) ByteSize() int { 96 return i.byteSize 97 } 98 99 func TestPreventsAddingItemsLargerThanBatchMaxByteSize(t *testing.T) { 100 ctx := context.Background() 101 itemType := reflect.TypeOf(&sizableItem{}) 102 b := batcher.New(itemType, &batcher.Options{MaxBatchByteSize: 1}, func(items interface{}) error { 103 return nil 104 }) 105 106 err := b.Add(ctx, &sizableItem{2}) 107 e := batcher.ErrMessageTooLarge 108 if err != e { 109 t.Errorf("got %v, want %v", err, e) 110 } 111 112 err = b.Add(ctx, &sizableItem{1}) 113 if err != nil { 114 t.Errorf("got error %v, want nil", err) 115 } 116 } 117 118 func TestBatchingConsidersMaxSizeAndMaxByteSize(t *testing.T) { 119 ctx := context.Background() 120 itemType := reflect.TypeOf(&sizableItem{}) 121 tests := []struct { 122 itemCount int 123 itemSize int 124 opts *batcher.Options 125 wantBatchCount int 126 }{ 127 {10, 0, &batcher.Options{MaxBatchSize: 2, MinBatchSize: 2}, 5}, 128 {10, 10, &batcher.Options{MaxBatchByteSize: 10, MinBatchSize: 1}, 10}, 129 {10, 5, &batcher.Options{MaxBatchByteSize: 10, MinBatchSize: 2}, 5}, 130 } 131 132 for _, test := range tests { 133 var got [][]*sizableItem 134 b := batcher.New(itemType, test.opts, func(items interface{}) error { 135 got = append(got, items.([]*sizableItem)) 136 return nil 137 }) 138 139 var wg sync.WaitGroup 140 item := &sizableItem{test.itemSize} 141 for i := 0; i < test.itemCount; i++ { 142 wg.Add(1) 143 go func() { 144 defer wg.Done() 145 if err := b.Add(ctx, item); err != nil { 146 t.Errorf("b.Add(ctx, item) error: %v", err) 147 } 148 }() 149 } 150 wg.Wait() 151 if len(got) != test.wantBatchCount { 152 t.Errorf("got %d batches, want %d", len(got), test.wantBatchCount) 153 } 154 } 155 } 156 157 func TestMinBatchSize(t *testing.T) { 158 // Verify the MinBatchSize option works. 159 var got [][]int 160 b := batcher.New(reflect.TypeOf(int(0)), &batcher.Options{MinBatchSize: 3}, func(items interface{}) error { 161 got = append(got, items.([]int)) 162 return nil 163 }) 164 for i := 0; i < 6; i++ { 165 b.AddNoWait(i) 166 } 167 b.Shutdown() 168 want := [][]int{{0, 1, 2}, {3, 4, 5}} 169 if !cmp.Equal(got, want) { 170 t.Errorf("got %+v, want %+v", got, want) 171 } 172 } 173 174 func TestSaturation(t *testing.T) { 175 // Verify that under high load the maximum number of handlers are running. 176 ctx := context.Background() 177 const ( 178 maxHandlers = 10 179 maxBatchSize = 50 180 ) 181 var ( 182 mu sync.Mutex 183 outstanding, max int // number of handlers 184 maxBatch int // size of largest batch 185 count = map[int]int{} // how many of each item the handlers observe 186 ) 187 b := batcher.New(reflect.TypeOf(int(0)), &batcher.Options{MaxHandlers: maxHandlers, MaxBatchSize: maxBatchSize}, func(x interface{}) error { 188 items := x.([]int) 189 mu.Lock() 190 outstanding++ 191 if outstanding > max { 192 max = outstanding 193 } 194 for _, x := range items { 195 count[x]++ 196 } 197 if len(items) > maxBatch { 198 maxBatch = len(items) 199 } 200 mu.Unlock() 201 defer func() { mu.Lock(); outstanding--; mu.Unlock() }() 202 // Sleep a little to increase the likelihood of saturation. 203 time.Sleep(10 * time.Millisecond) 204 return nil 205 }) 206 var wg sync.WaitGroup 207 const nItems = 1000 208 for i := 0; i < nItems; i++ { 209 i := i 210 wg.Add(1) 211 go func() { 212 defer wg.Done() 213 // Sleep a little to increase the likelihood of saturation. 214 time.Sleep(time.Millisecond) 215 if err := b.Add(ctx, i); err != nil { 216 t.Errorf("b.Add(ctx, %d) error: %v", i, err) 217 } 218 }() 219 } 220 wg.Wait() 221 // Check that we saturated the batcher. 222 if max != maxHandlers { 223 t.Errorf("max concurrent handlers = %d, want %d", max, maxHandlers) 224 } 225 // Check that at least one batch had more than one item. 226 if maxBatch <= 1 || maxBatch > maxBatchSize { 227 t.Errorf("got max batch size of %d, expected > 1 and <= %d", maxBatch, maxBatchSize) 228 } 229 // Check that handlers saw every item exactly once. 230 want := map[int]int{} 231 for i := 0; i < nItems; i++ { 232 want[i] = 1 233 } 234 if diff := cmp.Diff(count, want); diff != "" { 235 t.Errorf("items: %s", diff) 236 } 237 } 238 239 func TestShutdown(t *testing.T) { 240 ctx := context.Background() 241 var nHandlers int64 // atomic 242 c := make(chan int, 10) 243 b := batcher.New(reflect.TypeOf(int(0)), &batcher.Options{MaxHandlers: cap(c)}, func(x interface{}) error { 244 for range x.([]int) { 245 c <- 0 246 } 247 atomic.AddInt64(&nHandlers, 1) 248 defer atomic.AddInt64(&nHandlers, -1) 249 time.Sleep(time.Second) // we want handlers to be active on Shutdown 250 return nil 251 }) 252 for i := 0; i < cap(c); i++ { 253 go func() { 254 err := b.Add(ctx, 0) 255 if err != nil { 256 t.Errorf("b.Add error: %v", err) 257 } 258 }() 259 } 260 // Make sure all goroutines have started. 261 for i := 0; i < cap(c); i++ { 262 <-c 263 } 264 b.Shutdown() 265 266 if got := atomic.LoadInt64(&nHandlers); got != 0 { 267 t.Fatalf("%d Handlers still active after Shutdown returns", got) 268 } 269 if err := b.Add(ctx, 1); err == nil { 270 t.Error("got nil, want error from Add after Shutdown") 271 } 272 } 273 274 func TestItemCanBeInterface(t *testing.T) { 275 readerType := reflect.TypeOf([]io.Reader{}).Elem() 276 called := false 277 b := batcher.New(readerType, nil, func(items interface{}) error { 278 called = true 279 _, ok := items.([]io.Reader) 280 if !ok { 281 t.Fatal("items is not a []io.Reader") 282 } 283 return nil 284 }) 285 b.Add(context.Background(), &bytes.Buffer{}) 286 if !called { 287 t.Fatal("handler not called") 288 } 289 }