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  }