github.com/matrixorigin/matrixone@v0.7.0/pkg/util/batchpipe/batch_pipe_test.go (about)

     1  // Copyright 2022 Matrix Origin
     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  //      http://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 batchpipe
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"strconv"
    23  	"sync"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  
    30  	"github.com/matrixorigin/matrixone/pkg/common/moerr"
    31  )
    32  
    33  func waitChTimeout[T any](
    34  	ch <-chan T,
    35  	onRecvCheck func(element T, closed bool) (goOn bool, err error),
    36  	after time.Duration,
    37  ) error {
    38  	timeout := time.After(after)
    39  	for {
    40  		select {
    41  		case <-timeout:
    42  			return moerr.NewInternalError(context.TODO(), "timeout")
    43  		case item, ok := <-ch:
    44  			goOn, err := onRecvCheck(item, !ok)
    45  			if err != nil {
    46  				return err
    47  			}
    48  			if !ok || !goOn {
    49  				return nil
    50  			}
    51  		}
    52  	}
    53  }
    54  
    55  type Pos struct {
    56  	line    int
    57  	linepos int
    58  	docpos  int
    59  }
    60  
    61  const (
    62  	T_INT = "tst__int"
    63  	T_POS = "tst__pos"
    64  )
    65  
    66  type TestItem struct {
    67  	name   string
    68  	intval int
    69  	posval *Pos
    70  }
    71  
    72  func (item *TestItem) GetName() string { return item.name }
    73  
    74  var _ ItemBuffer[*TestItem, string] = (*intBuf)(nil)
    75  
    76  type intBuf struct {
    77  	Reminder
    78  	sum int
    79  }
    80  
    81  func (b *intBuf) Add(x *TestItem) { b.sum += x.intval }
    82  
    83  func (b *intBuf) Reset() { b.sum = 0; b.RemindReset() }
    84  
    85  func (b *intBuf) IsEmpty() bool { return b.sum == 0 }
    86  
    87  func (b *intBuf) ShouldFlush() bool { return b.sum > 100 }
    88  
    89  func (b *intBuf) GetBatch(_ context.Context, _ *bytes.Buffer) string {
    90  	return fmt.Sprintf("Batch int %d", b.sum)
    91  }
    92  
    93  var _ ItemBuffer[*TestItem, string] = (*posBuf)(nil)
    94  
    95  type posBuf struct {
    96  	Reminder
    97  	posList  []*Pos
    98  	wakeupCh chan<- time.Time
    99  }
   100  
   101  func (b *posBuf) Add(item *TestItem) {
   102  	b.posList = append(b.posList, item.posval)
   103  }
   104  
   105  func (b *posBuf) Reset() { b.posList = b.posList[:0]; b.RemindReset() }
   106  
   107  func (b *posBuf) IsEmpty() bool {
   108  	select {
   109  	case b.wakeupCh <- time.Now(): // when the reminder fires, it will check IsEmpty
   110  	default:
   111  	}
   112  	return len(b.posList) == 0
   113  }
   114  
   115  func (b *posBuf) ShouldFlush() bool { return len(b.posList) > 3 }
   116  
   117  // bytes.Buffer to mitigate mem allocaction and the return bytes should own its data
   118  func (b *posBuf) GetBatch(ctx context.Context, buf *bytes.Buffer) string {
   119  	buf.Reset()
   120  	for _, pos := range b.posList {
   121  		buf.WriteString(fmt.Sprintf("Ln %d, Col %d, Doc %d\n", pos.line, pos.linepos, pos.docpos))
   122  	}
   123  	return buf.String()
   124  }
   125  
   126  var _ PipeImpl[*TestItem, string] = &testCollector{}
   127  
   128  type testCollector struct {
   129  	sync.Mutex
   130  	*BaseBatchPipe[*TestItem, string]
   131  	receivedString []string
   132  	posBufWakeupCh chan time.Time
   133  	notify4Batch   func()
   134  }
   135  
   136  // create a new buffer for one kind of Item
   137  func (c *testCollector) NewItemBuffer(name string) ItemBuffer[*TestItem, string] {
   138  	switch name {
   139  	case T_INT:
   140  		return &intBuf{
   141  			Reminder: NewConstantClock(0),
   142  		}
   143  	case T_POS:
   144  		return &posBuf{
   145  			Reminder: NewExpBackOffClock(30*time.Millisecond, 300*time.Millisecond, 2),
   146  			wakeupCh: c.posBufWakeupCh,
   147  		}
   148  	}
   149  	panic("unrecognized name")
   150  }
   151  
   152  // BatchHandler handle the StoreBatch from a ItemBuffer, for example, execute inster sql
   153  // this handle may be running on multiple gorutine
   154  func (c *testCollector) NewItemBatchHandler(ctx context.Context) func(batch string) {
   155  	return func(batch string) {
   156  		c.Lock()
   157  		defer c.Unlock()
   158  		c.receivedString = append(c.receivedString, batch)
   159  		if c.notify4Batch != nil {
   160  			c.notify4Batch()
   161  		}
   162  	}
   163  }
   164  
   165  func (c *testCollector) Received() []string {
   166  	c.Lock()
   167  	defer c.Unlock()
   168  	return c.receivedString[:]
   169  }
   170  
   171  func newTestCollector(opts ...BaseBatchPipeOpt) *testCollector {
   172  	collector := &testCollector{
   173  		receivedString: make([]string, 0),
   174  		posBufWakeupCh: make(chan time.Time, 10),
   175  	}
   176  	base := NewBaseBatchPipe[*TestItem, string](collector, opts...)
   177  	collector.BaseBatchPipe = base
   178  	return collector
   179  }
   180  
   181  func TestBaseCollector(t *testing.T) {
   182  	ctx := context.TODO()
   183  	collector := newTestCollector(PipeWithBatchWorkerNum(1))
   184  	require.True(t, collector.Start(context.TODO()))
   185  	require.False(t, collector.Start(context.TODO()))
   186  	err := collector.SendItem(ctx,
   187  		&TestItem{name: T_INT, intval: 32},
   188  		&TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}},
   189  		&TestItem{name: T_INT, intval: 33},
   190  	)
   191  	require.NoError(t, err)
   192  	time.Sleep(50 * time.Millisecond)
   193  	require.Contains(t, collector.Received(), fmt.Sprintf("Ln %d, Col %d, Doc %d\n", 1, 12, 12))
   194  
   195  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   196  	time.Sleep(20 * time.Millisecond)
   197  	require.Contains(t, collector.Received(), "Batch int 105")
   198  
   199  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   200  	handle, succ := collector.Stop(false)
   201  	require.True(t, succ)
   202  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   203  		assert.True(t, closed)
   204  		return
   205  	}, time.Second))
   206  	require.Equal(t, 2, len(collector.Received()))
   207  }
   208  
   209  func TestBaseCollectorReminderBackOff(t *testing.T) {
   210  	ctx := context.TODO()
   211  	collector := newTestCollector(PipeWithBatchWorkerNum(1))
   212  	require.True(t, collector.Start(context.TODO()))
   213  	err := collector.SendItem(ctx, &TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}})
   214  	require.NoError(t, err)
   215  
   216  	var prev time.Time
   217  	gap := 30 // time.Millisecond
   218  	waitTimeCheck := func(element time.Time, closed bool) (goOn bool, err error) {
   219  		if gap >= 300 {
   220  			return
   221  		}
   222  		if prev.IsZero() {
   223  			goOn = true
   224  			prev = element
   225  		} else {
   226  			goOn = assert.InDelta(t, gap, element.Sub(prev).Milliseconds(), 30)
   227  			prev = element
   228  			gap *= 2
   229  		}
   230  		return
   231  	}
   232  	require.NoError(t, waitChTimeout(collector.posBufWakeupCh, waitTimeCheck, time.Second))
   233  
   234  	require.Contains(t, collector.Received(), fmt.Sprintf("Ln %d, Col %d, Doc %d\n", 1, 12, 12))
   235  
   236  	_ = collector.SendItem(ctx, &TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}})
   237  
   238  	// new write will reset timer to 30ms
   239  	time.Sleep(50 * time.Millisecond)
   240  	require.Contains(t, collector.Received(), fmt.Sprintf("Ln %d, Col %d, Doc %d\n", 1, 12, 12))
   241  
   242  	handle, succ := collector.Stop(false)
   243  	require.True(t, succ)
   244  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   245  		assert.True(t, closed)
   246  		return
   247  	}, time.Second))
   248  }
   249  
   250  func TestBaseCollectorGracefulStop(t *testing.T) {
   251  	var notify sync.WaitGroup
   252  	var notifyFun = func() {
   253  		notify.Done()
   254  	}
   255  	ctx := context.TODO()
   256  	collector := newTestCollector(PipeWithBatchWorkerNum(2), PipeWithBufferWorkerNum(1))
   257  	collector.notify4Batch = notifyFun
   258  	collector.Start(context.TODO())
   259  
   260  	notify.Add(1)
   261  	err := collector.SendItem(ctx,
   262  		&TestItem{name: T_INT, intval: 32},
   263  		&TestItem{name: T_INT, intval: 40},
   264  		&TestItem{name: T_INT, intval: 33},
   265  	)
   266  	notify.Wait()
   267  	require.NoError(t, err)
   268  	time.Sleep(10 * time.Millisecond)
   269  	require.Contains(t, collector.Received(), "Batch int 105")
   270  
   271  	notify.Add(1)
   272  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   273  	handle, succ := collector.Stop(true)
   274  	require.True(t, succ)
   275  	handle2, succ2 := collector.Stop(true)
   276  	require.False(t, succ2)
   277  	require.Nil(t, handle2)
   278  	notify.Add(1)
   279  	require.Error(t, collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40}))
   280  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   281  		assert.True(t, closed)
   282  		return
   283  	}, time.Second))
   284  	require.Contains(t, collector.Received(), "Batch int 40")
   285  }
   286  
   287  func TestBaseReminder(t *testing.T) {
   288  	ms := time.Millisecond
   289  	registry := newReminderRegistry()
   290  	registry.Register("1", 1*ms)
   291  	registry.Register("2", 0)
   292  	require.NotPanics(t, func() { registry.Register("1", 0*ms) })
   293  	require.Panics(t, func() { registry.Register("1", 1*ms) })
   294  	checkOneRecevied := func(_ string, closed bool) (goOn bool, err error) {
   295  		if closed {
   296  			err = moerr.NewInternalError(context.TODO(), "unexpected close")
   297  		}
   298  		return
   299  	}
   300  	// only one event 1 will be triggered
   301  	require.NoError(t, waitChTimeout(registry.C, checkOneRecevied, 500*ms))
   302  	require.Error(t, waitChTimeout(registry.C, checkOneRecevied, 500*ms))
   303  
   304  	// nothing happens after these two lines
   305  	registry.Reset("2", 2*ms)                                            // 2 is not in the registry
   306  	registry.Reset("1", 0*ms)                                            // 0 is ignored
   307  	require.Error(t, waitChTimeout(registry.C, checkOneRecevied, 50*ms)) // timeout
   308  	registry.Reset("1", 5*ms)
   309  	require.NoError(t, waitChTimeout(registry.C, checkOneRecevied, 500*ms))
   310  	registry.CleanAll()
   311  }
   312  
   313  func TestBaseRemind2(t *testing.T) {
   314  	ms := time.Millisecond
   315  	r := newReminderRegistry()
   316  	cases := []*struct {
   317  		id     string
   318  		d      []time.Duration
   319  		offset int
   320  	}{
   321  		{"0", []time.Duration{11 * ms, 8 * ms, 25 * ms}, 0},
   322  		{"1", []time.Duration{7 * ms, 15 * ms, 16 * ms}, 0},
   323  		{"2", []time.Duration{3 * ms, 12 * ms, 11 * ms}, 0},
   324  	}
   325  
   326  	type item struct {
   327  		d  time.Duration
   328  		id string
   329  	}
   330  
   331  	seq := []item{}
   332  
   333  	for _, c := range cases {
   334  		r.Register(c.id, c.d[c.offset])
   335  		c.offset += 1
   336  		t := 0 * ms
   337  		for _, delta := range c.d {
   338  			t += delta
   339  			seq = append(seq, item{t, c.id})
   340  		}
   341  	}
   342  
   343  	sort.Slice(seq, func(i, j int) bool {
   344  		return int64(seq[i].d) < int64(seq[j].d)
   345  	})
   346  
   347  	gotids := make([]string, 0, 9)
   348  	for i := 0; i < 9; i++ {
   349  		id := <-r.C
   350  		gotids = append(gotids, id)
   351  		idx, _ := strconv.ParseInt(id, 10, 32)
   352  		c := cases[idx]
   353  		if c.offset < 3 {
   354  			r.Reset(id, c.d[c.offset])
   355  			c.offset++
   356  		}
   357  	}
   358  
   359  	diff := 0
   360  	for i := range gotids {
   361  		if gotids[i] != seq[i].id {
   362  			diff++
   363  		}
   364  	}
   365  
   366  	// Appending to reminder.C happens in a goroutine, considering its scheduling latency,
   367  	// here we tolerate the disorder for 2 pairs
   368  	if diff > 4 {
   369  		t.Errorf("bad order of the events, want %v, got %s", seq, gotids)
   370  	}
   371  }
   372  
   373  func TestBaseBackOff(t *testing.T) {
   374  	ms := time.Millisecond
   375  	exp := NewExpBackOffClock(ms, 20*ms, 2)
   376  	for expect := ms; expect <= 20*ms; expect *= time.Duration(2) {
   377  		assert.Equal(t, exp.RemindNextAfter(), expect)
   378  		exp.RemindBackOff()
   379  	}
   380  	exp.RemindBackOff()
   381  	assert.Equal(t, 20*ms, exp.RemindNextAfter())
   382  }