github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/generic/buffer_system_window_test.go (about) 1 package generic 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/Jeffail/benthos/v3/public/bloblang" 13 "github.com/Jeffail/benthos/v3/public/service" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 ) 17 18 func TestSystemWindowBufferConfigs(t *testing.T) { 19 tests := []struct { 20 config string 21 lintErrContains string 22 buildErrContains string 23 }{ 24 { 25 config: ` 26 system_window: 27 size: 60m 28 `, 29 }, 30 { 31 config: ` 32 system_window: {} 33 `, 34 lintErrContains: "field size is required", 35 }, 36 { 37 config: ` 38 system_window: 39 timestamp_mapping: 'root =' 40 size: 60m 41 `, 42 lintErrContains: "expected whitespace", 43 }, 44 { 45 config: ` 46 system_window: 47 size: 60m 48 slide: 5m 49 offset: 1m 50 allowed_lateness: 2m 51 `, 52 }, 53 { 54 config: ` 55 system_window: 56 size: 60m 57 slide: 120m 58 offset: 1m 59 allowed_lateness: 2m 60 `, 61 buildErrContains: "invalid window slide", 62 }, 63 { 64 config: ` 65 system_window: 66 size: 60m 67 offset: 60m 68 allowed_lateness: 2m 69 `, 70 buildErrContains: "invalid offset", 71 }, 72 { 73 config: ` 74 system_window: 75 size: 60m 76 slide: 10m 77 offset: 10m 78 allowed_lateness: 2m 79 `, 80 buildErrContains: "invalid offset", 81 }, 82 { 83 config: ` 84 system_window: 85 size: 60m 86 slide: 10m 87 allowed_lateness: 200m 88 `, 89 buildErrContains: "invalid allowed_lateness", 90 }, 91 } 92 93 for i, test := range tests { 94 t.Run(strconv.Itoa(i), func(t *testing.T) { 95 env := service.NewStreamBuilder() 96 require.NoError(t, env.SetLoggerYAML(`level: OFF`)) 97 err := env.AddConsumerFunc(func(context.Context, *service.Message) error { 98 return nil 99 }) 100 require.NoError(t, err) 101 _, err = env.AddProducerFunc() 102 require.NoError(t, err) 103 104 err = env.SetBufferYAML(test.config) 105 if test.lintErrContains != "" { 106 require.Error(t, err) 107 assert.Contains(t, err.Error(), test.lintErrContains) 108 return 109 } 110 require.NoError(t, err) 111 112 strm, err := env.Build() 113 require.NoError(t, err) 114 115 cancelledCtx, done := context.WithCancel(context.Background()) 116 done() 117 err = strm.Run(cancelledCtx) 118 if test.buildErrContains != "" { 119 require.Error(t, err) 120 assert.Contains(t, err.Error(), test.buildErrContains) 121 return 122 } 123 require.EqualError(t, err, "context canceled") 124 require.NoError(t, strm.StopWithin(time.Second)) 125 }) 126 } 127 } 128 129 func TestSystemCurrentWindowCalc(t *testing.T) { 130 tests := []struct { 131 now string 132 size, slide, offset time.Duration 133 prevStart, prevEnd, start, end string 134 }{ 135 { 136 now: `2006-01-02T15:00:00Z`, 137 size: time.Hour, 138 start: `2006-01-02T14:00:00.000000001Z`, 139 end: `2006-01-02T15:00:00Z`, 140 prevStart: `2006-01-02T13:00:00.000000001Z`, 141 prevEnd: `2006-01-02T14:00:00Z`, 142 }, 143 { 144 now: `2006-01-02T15:00:00.000000001Z`, 145 size: time.Hour, 146 start: `2006-01-02T15:00:00.000000001Z`, 147 end: `2006-01-02T16:00:00Z`, 148 prevStart: `2006-01-02T14:00:00.000000001Z`, 149 prevEnd: `2006-01-02T15:00:00Z`, 150 }, 151 { 152 now: `2006-01-02T15:04:05.123456789Z`, 153 size: time.Hour, 154 start: `2006-01-02T15:00:00.000000001Z`, 155 end: `2006-01-02T16:00:00Z`, 156 prevStart: `2006-01-02T14:00:00.000000001Z`, 157 prevEnd: `2006-01-02T15:00:00Z`, 158 }, 159 { 160 now: `2006-01-02T15:34:05.123456789Z`, 161 size: time.Hour, 162 start: `2006-01-02T15:00:00.000000001Z`, 163 end: `2006-01-02T16:00:00Z`, 164 prevStart: `2006-01-02T14:00:00.000000001Z`, 165 prevEnd: `2006-01-02T15:00:00Z`, 166 }, 167 { 168 now: `2006-01-02T00:04:05.123456789Z`, 169 size: time.Hour, 170 start: `2006-01-02T00:00:00.000000001Z`, 171 end: `2006-01-02T01:00:00Z`, 172 prevStart: `2006-01-01T23:00:00.000000001Z`, 173 prevEnd: `2006-01-02T00:00:00Z`, 174 }, 175 { 176 now: `2006-01-02T15:04:05.123456789Z`, 177 size: time.Hour, 178 slide: time.Minute * 10, 179 start: `2006-01-02T14:10:00.000000001Z`, 180 end: `2006-01-02T15:10:00Z`, 181 prevStart: `2006-01-02T14:00:00.000000001Z`, 182 prevEnd: `2006-01-02T15:00:00Z`, 183 }, 184 { 185 now: `2006-01-02T15:04:05.123456789Z`, 186 size: time.Hour, 187 offset: time.Minute * 30, 188 start: `2006-01-02T14:30:00.000000001Z`, 189 end: `2006-01-02T15:30:00Z`, 190 prevStart: `2006-01-02T13:30:00.000000001Z`, 191 prevEnd: `2006-01-02T14:30:00Z`, 192 }, 193 { 194 now: `2006-01-02T15:04:05.123456789Z`, 195 size: time.Hour, 196 slide: time.Minute * 10, 197 offset: time.Minute * 5, 198 start: `2006-01-02T14:05:00.000000001Z`, 199 end: `2006-01-02T15:05:00Z`, 200 prevStart: `2006-01-02T13:55:00.000000001Z`, 201 prevEnd: `2006-01-02T14:55:00Z`, 202 }, 203 { 204 now: `2006-01-02T15:09:59.123456789Z`, 205 size: time.Hour, 206 slide: time.Minute * 10, 207 offset: time.Minute * 5, 208 start: `2006-01-02T14:15:00.000000001Z`, 209 end: `2006-01-02T15:15:00Z`, 210 prevStart: `2006-01-02T14:05:00.000000001Z`, 211 prevEnd: `2006-01-02T15:05:00Z`, 212 }, 213 } 214 215 for i, test := range tests { 216 t.Run(strconv.Itoa(i), func(t *testing.T) { 217 w, err := newSystemWindowBuffer(nil, func() time.Time { 218 ts, err := time.Parse(time.RFC3339Nano, test.now) 219 require.NoError(t, err) 220 return ts.UTC() 221 }, test.size, test.slide, test.offset, 0, nil) 222 require.NoError(t, err) 223 224 prevStart, prevEnd, start, end := w.nextSystemWindow() 225 226 assert.Equal(t, test.start, start.Format(time.RFC3339Nano), "start") 227 assert.Equal(t, test.end, end.Format(time.RFC3339Nano), "end") 228 assert.Equal(t, test.prevStart, prevStart.Format(time.RFC3339Nano), "prevStart") 229 assert.Equal(t, test.prevEnd, prevEnd.Format(time.RFC3339Nano), "prevEnd") 230 }) 231 } 232 } 233 234 func noopAck(context.Context, error) error { 235 return nil 236 } 237 238 func TestSystemWindowWritePurge(t *testing.T) { 239 mapping, err := bloblang.Parse(`root = this.ts`) 240 require.NoError(t, err) 241 242 currentTS := time.Unix(10, 1).UTC() 243 w, err := newSystemWindowBuffer(mapping, func() time.Time { 244 return currentTS 245 }, time.Second, 0, 0, 0, nil) 246 require.NoError(t, err) 247 248 err = w.WriteBatch(context.Background(), service.MessageBatch{ 249 service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)), 250 service.NewMessage([]byte(`{"id":"2","ts":8.5}`)), 251 service.NewMessage([]byte(`{"id":"3","ts":9.5}`)), 252 service.NewMessage([]byte(`{"id":"4","ts":10.5}`)), 253 }, noopAck) 254 require.NoError(t, err) 255 assert.Len(t, w.pending, 4) 256 257 err = w.WriteBatch(context.Background(), service.MessageBatch{ 258 service.NewMessage([]byte(`{"id":"5","ts":10.6}`)), 259 service.NewMessage([]byte(`{"id":"6","ts":10.7}`)), 260 service.NewMessage([]byte(`{"id":"7","ts":10.8}`)), 261 service.NewMessage([]byte(`{"id":"8","ts":10.9}`)), 262 }, noopAck) 263 require.NoError(t, err) 264 assert.Len(t, w.pending, 6) 265 } 266 267 func TestSystemWindowCreation(t *testing.T) { 268 mapping, err := bloblang.Parse(`root = this.ts`) 269 require.NoError(t, err) 270 271 currentTS := time.Unix(10, 1).UTC() 272 w, err := newSystemWindowBuffer(mapping, func() time.Time { 273 return currentTS 274 }, time.Second, 0, 0, 0, nil) 275 require.NoError(t, err) 276 277 err = w.WriteBatch(context.Background(), service.MessageBatch{ 278 service.NewMessage([]byte(`{"id":"1","ts":7.999999999}`)), 279 service.NewMessage([]byte(`{"id":"2","ts":8.5}`)), 280 service.NewMessage([]byte(`{"id":"3","ts":9.5}`)), 281 service.NewMessage([]byte(`{"id":"4","ts":10.5}`)), 282 }, noopAck) 283 require.NoError(t, err) 284 assert.Len(t, w.pending, 4) 285 286 resBatch, _, err := w.ReadBatch(context.Background()) 287 require.NoError(t, err) 288 289 require.Len(t, resBatch, 1) 290 msgBytes, err := resBatch[0].AsBytes() 291 require.NoError(t, err) 292 assert.Equal(t, `{"id":"3","ts":9.5}`, string(msgBytes)) 293 294 assert.Len(t, w.pending, 1) 295 assert.Equal(t, "1970-01-01T00:00:10Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) 296 297 currentTS = time.Unix(10, 999999100).UTC() 298 299 resBatch, _, err = w.ReadBatch(context.Background()) 300 require.NoError(t, err) 301 302 require.Len(t, resBatch, 1) 303 msgBytes, err = resBatch[0].AsBytes() 304 require.NoError(t, err) 305 assert.Equal(t, `{"id":"4","ts":10.5}`, string(msgBytes)) 306 307 assert.Len(t, w.pending, 0) 308 assert.Equal(t, "1970-01-01T00:00:11Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) 309 310 currentTS = time.Unix(11, 999999100).UTC() 311 312 smallWaitCtx, done := context.WithTimeout(context.Background(), time.Millisecond*50) 313 resBatch, _, err = w.ReadBatch(smallWaitCtx) 314 done() 315 require.Error(t, err) 316 assert.Len(t, resBatch, 0) 317 assert.Equal(t, "1970-01-01T00:00:12Z", w.latestFlushedWindowEnd.Format(time.RFC3339Nano)) 318 319 err = w.WriteBatch(context.Background(), service.MessageBatch{ 320 service.NewMessage([]byte(`{"id":"5","ts":8.1}`)), 321 service.NewMessage([]byte(`{"id":"6","ts":9.999999999}`)), 322 service.NewMessage([]byte(`{"id":"7","ts":10}`)), 323 service.NewMessage([]byte(`{"id":"8","ts":11.999999999}`)), 324 service.NewMessage([]byte(`{"id":"9","ts":12.1}`)), 325 service.NewMessage([]byte(`{"id":"10","ts":13}`)), 326 }, noopAck) 327 require.NoError(t, err) 328 require.Len(t, w.pending, 2) 329 330 msgBytes, err = w.pending[0].m.AsBytes() 331 require.NoError(t, err) 332 assert.Equal(t, `{"id":"9","ts":12.1}`, string(msgBytes)) 333 334 msgBytes, err = w.pending[1].m.AsBytes() 335 require.NoError(t, err) 336 assert.Equal(t, `{"id":"10","ts":13}`, string(msgBytes)) 337 } 338 339 func TestSystemWindowCreationSliding(t *testing.T) { 340 mapping, err := bloblang.Parse(`root = this.ts`) 341 require.NoError(t, err) 342 343 currentTS := time.Unix(10, 0).UTC() 344 w, err := newSystemWindowBuffer(mapping, func() time.Time { 345 return currentTS 346 }, time.Second, time.Millisecond*500, 0, 0, nil) 347 require.NoError(t, err) 348 w.latestFlushedWindowEnd = time.Unix(9, 500_000_000) 349 350 err = w.WriteBatch(context.Background(), service.MessageBatch{ 351 service.NewMessage([]byte(`{"id":"1","ts":9.85}`)), 352 service.NewMessage([]byte(`{"id":"2","ts":9.9}`)), 353 service.NewMessage([]byte(`{"id":"3","ts":10.15}`)), 354 service.NewMessage([]byte(`{"id":"4","ts":10.3}`)), 355 service.NewMessage([]byte(`{"id":"5","ts":10.5}`)), 356 service.NewMessage([]byte(`{"id":"6","ts":10.7}`)), 357 service.NewMessage([]byte(`{"id":"7","ts":10.9}`)), 358 service.NewMessage([]byte(`{"id":"8","ts":11.1}`)), 359 service.NewMessage([]byte(`{"id":"9","ts":11.35}`)), 360 service.NewMessage([]byte(`{"id":"10","ts":11.52}`)), 361 service.NewMessage([]byte(`{"id":"11","ts":11.8}`)), 362 }, noopAck) 363 require.NoError(t, err) 364 assert.Len(t, w.pending, 11) 365 366 assertBatchIndex := func(i int, batch service.MessageBatch, exp string) { 367 t.Helper() 368 require.True(t, len(batch) > i) 369 msgBytes, err := batch[i].AsBytes() 370 require.NoError(t, err) 371 assert.Equal(t, exp, string(msgBytes)) 372 } 373 374 resBatch, _, err := w.ReadBatch(context.Background()) 375 require.NoError(t, err) 376 377 assert.Len(t, resBatch, 2) 378 assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`) 379 assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`) 380 381 currentTS = time.Unix(10, 500000000).UTC() 382 resBatch, _, err = w.ReadBatch(context.Background()) 383 require.NoError(t, err) 384 385 assert.Len(t, resBatch, 5) 386 assertBatchIndex(0, resBatch, `{"id":"1","ts":9.85}`) 387 assertBatchIndex(1, resBatch, `{"id":"2","ts":9.9}`) 388 assertBatchIndex(2, resBatch, `{"id":"3","ts":10.15}`) 389 assertBatchIndex(3, resBatch, `{"id":"4","ts":10.3}`) 390 assertBatchIndex(4, resBatch, `{"id":"5","ts":10.5}`) 391 392 currentTS = time.Unix(11, 0).UTC() 393 resBatch, _, err = w.ReadBatch(context.Background()) 394 require.NoError(t, err) 395 396 assert.Len(t, resBatch, 5) 397 assertBatchIndex(0, resBatch, `{"id":"3","ts":10.15}`) 398 assertBatchIndex(1, resBatch, `{"id":"4","ts":10.3}`) 399 assertBatchIndex(2, resBatch, `{"id":"5","ts":10.5}`) 400 assertBatchIndex(3, resBatch, `{"id":"6","ts":10.7}`) 401 assertBatchIndex(4, resBatch, `{"id":"7","ts":10.9}`) 402 403 currentTS = time.Unix(11, 500_000_000).UTC() 404 resBatch, _, err = w.ReadBatch(context.Background()) 405 require.NoError(t, err) 406 407 assert.Len(t, resBatch, 4) 408 assertBatchIndex(0, resBatch, `{"id":"6","ts":10.7}`) 409 assertBatchIndex(1, resBatch, `{"id":"7","ts":10.9}`) 410 assertBatchIndex(2, resBatch, `{"id":"8","ts":11.1}`) 411 assertBatchIndex(3, resBatch, `{"id":"9","ts":11.35}`) 412 413 currentTS = time.Unix(12, 0).UTC() 414 resBatch, _, err = w.ReadBatch(context.Background()) 415 require.NoError(t, err) 416 417 assert.Len(t, resBatch, 4) 418 assertBatchIndex(0, resBatch, `{"id":"8","ts":11.1}`) 419 assertBatchIndex(1, resBatch, `{"id":"9","ts":11.35}`) 420 assertBatchIndex(2, resBatch, `{"id":"10","ts":11.52}`) 421 assertBatchIndex(3, resBatch, `{"id":"11","ts":11.8}`) 422 } 423 424 func TestSystemWindowAckOneToMany(t *testing.T) { 425 mapping, err := bloblang.Parse(`root = this.ts`) 426 require.NoError(t, err) 427 428 currentTS := time.Unix(10, 1).UTC() 429 w, err := newSystemWindowBuffer(mapping, func() time.Time { 430 return currentTS 431 }, time.Second, 0, 0, 0, nil) 432 require.NoError(t, err) 433 434 var ackCalled int 435 var ackErr error 436 437 require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ 438 service.NewMessage([]byte(`{"id":"1","ts":9.5}`)), 439 service.NewMessage([]byte(`{"id":"2","ts":10.5}`)), 440 service.NewMessage([]byte(`{"id":"3","ts":11.5}`)), 441 }, func(ctx context.Context, err error) error { 442 ackCalled++ 443 if err != nil { 444 ackErr = err 445 } 446 return nil 447 })) 448 449 ackFuncs := []service.AckFunc{} 450 451 resBatch, aFn, err := w.ReadBatch(context.Background()) 452 require.NoError(t, err) 453 require.Len(t, resBatch, 1) 454 msgBytes, err := resBatch[0].AsBytes() 455 require.NoError(t, err) 456 assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes)) 457 ackFuncs = append(ackFuncs, aFn) 458 459 currentTS = time.Unix(11, 0).UTC() 460 461 resBatch, aFn, err = w.ReadBatch(context.Background()) 462 require.NoError(t, err) 463 require.Len(t, resBatch, 1) 464 msgBytes, err = resBatch[0].AsBytes() 465 require.NoError(t, err) 466 assert.Equal(t, `{"id":"2","ts":10.5}`, string(msgBytes)) 467 ackFuncs = append(ackFuncs, aFn) 468 469 currentTS = time.Unix(12, 0).UTC() 470 471 resBatch, aFn, err = w.ReadBatch(context.Background()) 472 require.NoError(t, err) 473 require.Len(t, resBatch, 1) 474 msgBytes, err = resBatch[0].AsBytes() 475 require.NoError(t, err) 476 assert.Equal(t, `{"id":"3","ts":11.5}`, string(msgBytes)) 477 ackFuncs = append(ackFuncs, aFn) 478 479 require.Len(t, ackFuncs, 3) 480 assert.Equal(t, 0, ackCalled) 481 assert.NoError(t, ackErr) 482 483 require.NoError(t, ackFuncs[0](context.Background(), nil)) 484 assert.Equal(t, 0, ackCalled) 485 assert.NoError(t, ackErr) 486 487 require.NoError(t, ackFuncs[1](context.Background(), errors.New("custom error"))) 488 assert.Equal(t, 0, ackCalled) 489 assert.NoError(t, ackErr) 490 491 require.NoError(t, ackFuncs[2](context.Background(), nil)) 492 assert.Equal(t, 1, ackCalled) 493 assert.EqualError(t, ackErr, "custom error") 494 } 495 496 func TestSystemWindowAckManyToOne(t *testing.T) { 497 mapping, err := bloblang.Parse(`root = this.ts`) 498 require.NoError(t, err) 499 500 currentTS := time.Unix(10, 1).UTC() 501 w, err := newSystemWindowBuffer(mapping, func() time.Time { 502 return currentTS 503 }, time.Second, 0, 0, 0, nil) 504 require.NoError(t, err) 505 506 ackCalls := map[int]error{} 507 508 require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ 509 service.NewMessage([]byte(`{"id":"1","ts":9.5}`)), 510 }, func(ctx context.Context, err error) error { 511 ackCalls[0] = err 512 return nil 513 })) 514 515 require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ 516 service.NewMessage([]byte(`{"id":"2","ts":9.6}`)), 517 }, func(ctx context.Context, err error) error { 518 ackCalls[1] = err 519 return nil 520 })) 521 522 require.NoError(t, w.WriteBatch(context.Background(), service.MessageBatch{ 523 service.NewMessage([]byte(`{"id":"3","ts":9.7}`)), 524 }, func(ctx context.Context, err error) error { 525 ackCalls[2] = err 526 return nil 527 })) 528 529 resBatch, aFn, err := w.ReadBatch(context.Background()) 530 require.NoError(t, err) 531 require.Len(t, resBatch, 3) 532 533 msgBytes, err := resBatch[0].AsBytes() 534 require.NoError(t, err) 535 assert.Equal(t, `{"id":"1","ts":9.5}`, string(msgBytes)) 536 537 msgBytes, err = resBatch[1].AsBytes() 538 require.NoError(t, err) 539 assert.Equal(t, `{"id":"2","ts":9.6}`, string(msgBytes)) 540 541 msgBytes, err = resBatch[2].AsBytes() 542 require.NoError(t, err) 543 assert.Equal(t, `{"id":"3","ts":9.7}`, string(msgBytes)) 544 545 assert.Empty(t, ackCalls) 546 require.NoError(t, aFn(context.Background(), errors.New("custom error"))) 547 548 assert.Equal(t, map[int]error{ 549 0: errors.New("custom error"), 550 1: errors.New("custom error"), 551 2: errors.New("custom error"), 552 }, ackCalls) 553 } 554 555 func TestSystemWindowParallelReadAndWrites(t *testing.T) { 556 mapping, err := bloblang.Parse(`root = this.ts`) 557 require.NoError(t, err) 558 559 currentTS := time.Unix(10, 500000000).UTC() 560 w, err := newSystemWindowBuffer(mapping, func() time.Time { 561 return currentTS 562 }, time.Second, 0, 0, 0, nil) 563 require.NoError(t, err) 564 565 var wg sync.WaitGroup 566 wg.Add(2) 567 568 startChan := make(chan struct{}) 569 go func() { 570 defer wg.Done() 571 <-startChan 572 for i := 0; i < 1000; i++ { 573 msg := fmt.Sprintf(`{"id":"%v","ts":10.5}`, i) 574 writeErr := w.WriteBatch(context.Background(), service.MessageBatch{ 575 service.NewMessage([]byte(msg)), 576 }, func(ctx context.Context, err error) error { 577 return nil 578 }) 579 require.NoError(t, writeErr) 580 } 581 }() 582 go func() { 583 defer wg.Done() 584 <-startChan 585 _, _, readErr := w.ReadBatch(context.Background()) 586 require.NoError(t, readErr) 587 }() 588 589 close(startChan) 590 wg.Wait() 591 }