github.com/matrixorigin/matrixone@v1.2.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  func waitUtil(timeout, check_interval time.Duration, check func() bool) error {
    56  	timeoutTimer := time.After(timeout)
    57  	for {
    58  		select {
    59  		case <-timeoutTimer:
    60  			return moerr.NewInternalError(context.TODO(), "timeout")
    61  		default:
    62  			if check() {
    63  				return nil
    64  			}
    65  			time.Sleep(check_interval)
    66  		}
    67  	}
    68  }
    69  
    70  type Pos struct {
    71  	line    int
    72  	linepos int
    73  	docpos  int
    74  }
    75  
    76  const (
    77  	T_INT = "tst__int"
    78  	T_POS = "tst__pos"
    79  )
    80  
    81  type TestItem struct {
    82  	name   string
    83  	intval int
    84  	posval *Pos
    85  }
    86  
    87  func (item *TestItem) GetName() string { return item.name }
    88  
    89  var _ ItemBuffer[*TestItem, string] = (*intBuf)(nil)
    90  
    91  type intBuf struct {
    92  	Reminder
    93  	sum int
    94  }
    95  
    96  func (b *intBuf) Add(x *TestItem) { b.sum += x.intval }
    97  
    98  func (b *intBuf) Reset() { b.sum = 0; b.RemindReset() }
    99  
   100  func (b *intBuf) IsEmpty() bool { return b.sum == 0 }
   101  
   102  func (b *intBuf) ShouldFlush() bool { return b.sum > 100 }
   103  
   104  func (b *intBuf) GetBatch(_ context.Context, _ *bytes.Buffer) string {
   105  	return fmt.Sprintf("Batch int %d", b.sum)
   106  }
   107  
   108  var _ ItemBuffer[*TestItem, string] = (*posBuf)(nil)
   109  
   110  type posBuf struct {
   111  	Reminder
   112  	posList  []*Pos
   113  	wakeupCh chan<- time.Time
   114  }
   115  
   116  func (b *posBuf) Add(item *TestItem) {
   117  	b.posList = append(b.posList, item.posval)
   118  }
   119  
   120  func (b *posBuf) Reset() { b.posList = b.posList[:0]; b.RemindReset() }
   121  
   122  func (b *posBuf) IsEmpty() bool {
   123  	select {
   124  	case b.wakeupCh <- time.Now(): // when the reminder fires, it will check IsEmpty
   125  	default:
   126  	}
   127  	return len(b.posList) == 0
   128  }
   129  
   130  func (b *posBuf) ShouldFlush() bool { return len(b.posList) > 3 }
   131  
   132  // bytes.Buffer to mitigate mem allocaction and the return bytes should own its data
   133  func (b *posBuf) GetBatch(ctx context.Context, buf *bytes.Buffer) string {
   134  	buf.Reset()
   135  	for _, pos := range b.posList {
   136  		buf.WriteString(fmt.Sprintf("Ln %d, Col %d, Doc %d\n", pos.line, pos.linepos, pos.docpos))
   137  	}
   138  	return buf.String()
   139  }
   140  
   141  var _ PipeImpl[*TestItem, string] = &testCollector{}
   142  
   143  type testCollector struct {
   144  	sync.Mutex
   145  	*BaseBatchPipe[*TestItem, string]
   146  	receivedString []string
   147  	posBufWakeupCh chan time.Time
   148  	notify4Batch   func()
   149  }
   150  
   151  // create a new buffer for one kind of Item
   152  func (c *testCollector) NewItemBuffer(name string) ItemBuffer[*TestItem, string] {
   153  	switch name {
   154  	case T_INT:
   155  		return &intBuf{
   156  			Reminder: NewConstantClock(0),
   157  		}
   158  	case T_POS:
   159  		return &posBuf{
   160  			Reminder: NewExpBackOffClock(30*time.Millisecond, 300*time.Millisecond, 2),
   161  			wakeupCh: c.posBufWakeupCh,
   162  		}
   163  	}
   164  	panic("unrecognized name")
   165  }
   166  
   167  // BatchHandler handle the StoreBatch from a ItemBuffer, for example, execute inster sql
   168  // this handle may be running on multiple gorutine
   169  func (c *testCollector) NewItemBatchHandler(ctx context.Context) func(batch string) {
   170  	return func(batch string) {
   171  		c.Lock()
   172  		defer c.Unlock()
   173  		c.receivedString = append(c.receivedString, batch)
   174  		if c.notify4Batch != nil {
   175  			c.notify4Batch()
   176  		}
   177  	}
   178  }
   179  
   180  func (c *testCollector) Received() []string {
   181  	c.Lock()
   182  	defer c.Unlock()
   183  	return c.receivedString[:]
   184  }
   185  
   186  func (c *testCollector) ReceivedContains(item string) bool {
   187  	c.Lock()
   188  	defer c.Unlock()
   189  	for _, s := range c.receivedString[:] {
   190  		if s == item {
   191  			return true
   192  		}
   193  	}
   194  	return false
   195  }
   196  
   197  func newTestCollector(opts ...BaseBatchPipeOpt) *testCollector {
   198  	collector := &testCollector{
   199  		receivedString: make([]string, 0),
   200  		posBufWakeupCh: make(chan time.Time, 10),
   201  	}
   202  	base := NewBaseBatchPipe[*TestItem, string](collector, opts...)
   203  	collector.BaseBatchPipe = base
   204  	return collector
   205  }
   206  
   207  func TestBaseCollector(t *testing.T) {
   208  	ctx := context.TODO()
   209  	collector := newTestCollector(PipeWithBatchWorkerNum(1))
   210  	require.True(t, collector.Start(context.TODO()))
   211  	require.False(t, collector.Start(context.TODO()))
   212  	err := collector.SendItem(ctx,
   213  		&TestItem{name: T_INT, intval: 32},
   214  		&TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}},
   215  		&TestItem{name: T_INT, intval: 33},
   216  	)
   217  	require.NoError(t, err)
   218  	// after 30ms, flush pos type test item and we can find it
   219  	batchString := fmt.Sprintf("Ln %d, Col %d, Doc %d\n", 1, 12, 12)
   220  	err = waitUtil(60*time.Second, 30*time.Millisecond, func() bool {
   221  		return collector.ReceivedContains(batchString)
   222  	})
   223  	require.NoError(t, err)
   224  
   225  	// int sum is 100+, flush
   226  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   227  
   228  	err = waitUtil(60*time.Second, 30*time.Millisecond, func() bool {
   229  		return collector.ReceivedContains("Batch int 105")
   230  	})
   231  	require.NoError(t, err)
   232  
   233  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   234  	handle, succ := collector.Stop(false)
   235  	require.True(t, succ)
   236  	// no items are received after stopping
   237  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   238  		assert.True(t, closed)
   239  		return
   240  	}, time.Second))
   241  	require.Equal(t, 2, len(collector.Received()))
   242  }
   243  
   244  func TestBaseCollectorReminderBackOff(t *testing.T) {
   245  	ctx := context.TODO()
   246  	collector := newTestCollector(PipeWithBatchWorkerNum(1))
   247  	require.True(t, collector.Start(context.TODO()))
   248  	err := collector.SendItem(ctx, &TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}})
   249  	require.NoError(t, err)
   250  
   251  	// have to find 2 gaps bigger than 100ms to prove that backoff works well
   252  	bigBackOffCnt := 0
   253  	var prev time.Time
   254  	waitTimeCheck := func(element time.Time, closed bool) (goOn bool, err error) {
   255  		t.Log(element, bigBackOffCnt)
   256  		if bigBackOffCnt >= 2 {
   257  			return
   258  		}
   259  		if !prev.IsZero() && element.Sub(prev).Milliseconds() > 100 {
   260  			bigBackOffCnt += 1
   261  		}
   262  		goOn = true
   263  		prev = element
   264  		return
   265  	}
   266  
   267  	// wait on posBufWakeCh, the wakeup freq will be bigger and bigger until 300ms
   268  	require.NoError(t, waitChTimeout(collector.posBufWakeupCh, waitTimeCheck, 10*time.Second))
   269  
   270  	batchString := fmt.Sprintf("Ln %d, Col %d, Doc %d\n", 1, 12, 12)
   271  	err = waitUtil(60*time.Second, 10*time.Millisecond, func() bool {
   272  		return collector.ReceivedContains(batchString)
   273  	})
   274  	require.NoError(t, err)
   275  
   276  	_ = collector.SendItem(ctx, &TestItem{name: T_POS, posval: &Pos{line: 1, linepos: 12, docpos: 12}})
   277  
   278  	// new write will reset timer to 30ms, so it should be received quickly, but…… what do we know about the machine?
   279  	err = waitUtil(60*time.Second, 10*time.Millisecond, func() bool {
   280  		return collector.ReceivedContains(batchString)
   281  	})
   282  	require.NoError(t, err)
   283  
   284  	handle, succ := collector.Stop(false)
   285  	require.True(t, succ)
   286  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   287  		assert.True(t, closed)
   288  		return
   289  	}, time.Second))
   290  }
   291  
   292  func TestBaseCollectorGracefulStop(t *testing.T) {
   293  	ctx := context.TODO()
   294  	collector := newTestCollector(PipeWithBatchWorkerNum(2), PipeWithBufferWorkerNum(1))
   295  	collector.Start(context.TODO())
   296  
   297  	err := collector.SendItem(ctx,
   298  		&TestItem{name: T_INT, intval: 32},
   299  		&TestItem{name: T_INT, intval: 40},
   300  		&TestItem{name: T_INT, intval: 33},
   301  	)
   302  	require.NoError(t, err)
   303  	err = waitUtil(60*time.Second, 10*time.Millisecond, func() bool {
   304  		return collector.ReceivedContains("Batch int 105")
   305  	})
   306  	require.NoError(t, err)
   307  
   308  	_ = collector.SendItem(ctx, &TestItem{name: T_INT, intval: 40})
   309  	// graceful stopping will wait completing sending
   310  	handle, succ := collector.Stop(true)
   311  	require.True(t, succ)
   312  	handle2, succ2 := collector.Stop(true)
   313  	require.False(t, succ2)
   314  	require.Nil(t, handle2)
   315  
   316  	// send after stopping
   317  	require.Error(t, collector.SendItem(ctx, &TestItem{name: T_INT, intval: 77}))
   318  	// no new value
   319  	require.NoError(t, waitChTimeout(handle, func(element struct{}, closed bool) (goOn bool, err error) {
   320  		assert.True(t, closed)
   321  		return
   322  	}, time.Second))
   323  	err = waitUtil(60*time.Second, 10*time.Millisecond, func() bool {
   324  		return collector.ReceivedContains("Batch int 40")
   325  	})
   326  	require.NoError(t, err)
   327  	t.Log(collector.Received())
   328  }
   329  
   330  func TestBaseReminder(t *testing.T) {
   331  	ms := time.Millisecond
   332  	registry := newReminderRegistry()
   333  	registry.Register("1", 1*ms)
   334  	registry.Register("2", 0)
   335  	require.NotPanics(t, func() { registry.Register("1", 0*ms) })
   336  	require.Panics(t, func() { registry.Register("1", 1*ms) })
   337  	checkOneRecevied := func(_ string, closed bool) (goOn bool, err error) {
   338  		if closed {
   339  			err = moerr.NewInternalError(context.TODO(), "unexpected close")
   340  		}
   341  		return
   342  	}
   343  	// only one event 1 will be triggered
   344  	require.NoError(t, waitChTimeout(registry.C, checkOneRecevied, 5000*ms))
   345  	require.Error(t, waitChTimeout(registry.C, checkOneRecevied, 500*ms)) // timeout
   346  
   347  	// nothing happens after these two lines
   348  	registry.Reset("2", 2*ms)                                            // 2 is not in the registry
   349  	registry.Reset("1", 0*ms)                                            // 0 is ignored
   350  	require.Error(t, waitChTimeout(registry.C, checkOneRecevied, 50*ms)) // timeout
   351  	registry.Reset("1", 5*ms)
   352  	require.NoError(t, waitChTimeout(registry.C, checkOneRecevied, 5000*ms))
   353  	registry.CleanAll()
   354  }
   355  
   356  func TestBaseRemind2(t *testing.T) {
   357  	ms := time.Millisecond
   358  	r := newReminderRegistry()
   359  	cases := []*struct {
   360  		id     string
   361  		d      []time.Duration
   362  		offset int
   363  	}{
   364  		{"0", []time.Duration{11 * ms, 8 * ms, 25 * ms}, 0},
   365  		{"1", []time.Duration{7 * ms, 15 * ms, 16 * ms}, 0},
   366  		{"2", []time.Duration{3 * ms, 12 * ms, 11 * ms}, 0},
   367  	}
   368  
   369  	type item struct {
   370  		d  time.Duration
   371  		id string
   372  	}
   373  
   374  	seq := []item{}
   375  
   376  	for _, c := range cases {
   377  		r.Register(c.id, c.d[c.offset])
   378  		c.offset += 1
   379  		t := 0 * ms
   380  		for _, delta := range c.d {
   381  			t += delta
   382  			seq = append(seq, item{t, c.id})
   383  		}
   384  	}
   385  
   386  	sort.Slice(seq, func(i, j int) bool {
   387  		return int64(seq[i].d) < int64(seq[j].d)
   388  	})
   389  
   390  	gotids := make([]string, 0, 9)
   391  	for i := 0; i < 9; i++ {
   392  		id := <-r.C
   393  		gotids = append(gotids, id)
   394  		idx, _ := strconv.ParseInt(id, 10, 32)
   395  		c := cases[idx]
   396  		if c.offset < 3 {
   397  			r.Reset(id, c.d[c.offset])
   398  			c.offset++
   399  		}
   400  	}
   401  
   402  	diff := 0
   403  	for i := range gotids {
   404  		if gotids[i] != seq[i].id {
   405  			diff++
   406  		}
   407  	}
   408  
   409  	// We have seen 9 events unitil now, it is ok.
   410  	// Appending to reminder.C happens in a goroutine, considering its scheduling latency,
   411  	// it is not reliable to check the order, so just print it
   412  	if diff > 6 {
   413  		t.Logf("bad order of the events, want %v, got %s", seq, gotids)
   414  	}
   415  }
   416  
   417  func TestBaseBackOff(t *testing.T) {
   418  	ms := time.Millisecond
   419  	exp := NewExpBackOffClock(ms, 20*ms, 2)
   420  	for expect := ms; expect <= 20*ms; expect *= time.Duration(2) {
   421  		require.Equal(t, exp.RemindNextAfter(), expect)
   422  		exp.RemindBackOff()
   423  	}
   424  	exp.RemindBackOff()
   425  	require.Equal(t, 20*ms, exp.RemindNextAfter())
   426  }