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 }