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 }